Почему С# неявно конвертируется в nullable DateTime, если нет неявного оператора для nullable DateTime?

Это вопрос о языке С# или, по крайней мере, о том, как этот язык реализован в Visual Studio.

Предположим, что у кого-то есть класс Foo, который определяет неявный оператор в System.DateTime

public static implicit operator DateTime(Foo item)

Рассмотрим следующий код:

Foo foo = SomeMethodWhichCanReturnNull();    
DateTime?  dtFoo = foo;

Что бы я ожидал: Невозможно скомпилировать жалобу, что нет преобразования от Foo до DateTime?.

То, что я нахожу: Компилятор фактически вызывает определенный неявный оператор от Foo до DateTime и сбой, когда ему передается нуль (это единственный способ, с помощью которого конвертер может реагировать на нуль).

Конечно, обход - это определить

public static implicit operator DateTime?(Foo item)

но зачем мне это делать? Разве не DateTime и DateTime? два разных типа?

Ответ 1

Во-первых, спецификация языка С# говорит, что встроенное неявное преобразование может быть вставлено с каждой стороны пользовательского неявного преобразования. Поэтому, если у вас есть пользовательское неявное преобразование от Shape до Giraffe, вам автоматически разрешается конвертировать из Square в Mammal, потому что он идет Square --> Shape --> Giraffe --> Mammal. В вашем случае дополнительное преобразование вставляется только с одной стороны, предполагая, что операнд имеет тип Foo. Существует неявное преобразование от любого типа к соответствующему типу NULL. Второе определяемое пользователем преобразование никогда не вставлено; только встроенные преобразования могут быть вставлены с обеих сторон.

(Я отмечаю, что это верно и для явных преобразований, определяемых пользователем, явные преобразования могут быть вставлены с каждой стороны.)

Во-вторых, вы нарушаете спецификацию, которая настоятельно предполагает, что пользовательское неявное преобразование никогда не должно генерировать исключение. Если вы не можете гарантировать, что операция будет успешной, либо изменить типы, либо сделать это явным преобразованием.

В-третьих, вам может быть интересно узнать, что компилятор С# автоматически определит "отмененное" преобразование, если оба типа в пользовательском неявном преобразовании являются типами с нулевым значением. Если у вас есть пользовательское неявное преобразование из типа структуры S в тип структуры T, вы получаете "отмененное" преобразование из S? к T? бесплатно, с семантикой s.HasValue ? new T?((T)s.Value) : new T?().

Этот вопрос является одной из наиболее сложных областей в спецификации С#, поэтому я рекомендую вам внимательно прочитать его, если вы хотите узнать точную информацию.

Ответ 2

DateTime? на самом деле является синтаксическим сахаром С#, представляющим Nullable<DateTime>. Когда вы пишете что-то вроде DateTime? dtFoo = foo, компилятор действительно генерирует код, например:

Nullable<DateTime> dtFoo = new Nullable<DateTime>(foo);

Что отлично от точки компилятора, поскольку конструктор с нулевым значением принимает DateTime как параметр, а foo имеет преобразование типа для него.

Ответ 3

Если у вас есть это:

Foo foo = SomeMethodWhichCanReturnNull();    
DateTime? dtFoo = foo;

Компилятор видит, что существует неявное преобразование от Foo до DateTime и неявное преобразование от DateTime до DateTime?. Он решает, что он может, таким образом, неявно преобразовывать из Foo в DateTime? и довольно успешно компилируется.

Оператор implicit не должен генерировать исключение. Тот факт, что вы нарушаете это, является причиной того, что вы видите запутанное поведение. Вероятно, вы должны изменить это на оператор explicit и, возможно, добавить преобразование implicit в DateTime?, которое возвращает null в случае null Foo.

Ответ 4

Это действует по той же причине, что вы можете сделать следующее:

DateTime time1 = new DateTime();
DateTime? time2 = time1;

Нормаль DateTime может быть преобразована в nullable DateTime. Это означает, что ваш класс Foo, который действует как DateTime, может следовать по пути Foo -> DateTime -> DateTime?

Я предполагаю, что DateTime? (или любой другой класс с нулевым уровнем) имеет неявный оператор для своего нормального типа.

Ответ 5

Прежде всего, помните, что T? (когда T a struct) на самом деле означает Nullable<T>.

Теперь сделаем короткую тестовую программу, которая реплицирует это:

using System;
class Program
{
    public static void Main(string[] args)
    {
        DateTime? x = new Program();
        Console.ReadKey(true);
    }
    public static implicit operator DateTime(Program x)
    {
        return new DateTime();
    }
}

И теперь посмотрим, что он компилирует (декомпилировался с помощью большого файла dotPeek):

using System;
internal class Program
{
  public Program()
  {
    base..ctor();
  }

  public static implicit operator DateTime(Program x)
  {
    return new DateTime();
  }

  public static void Main(string[] args)
  {
    DateTime? nullable = new DateTime?((DateTime) new Program()); 
    Console.ReadKey(true);
  }
}

Фактически, dotPeek мне нравится здесь: он должен быть тем же, но с DateTime? nullable = new DateTime?((DateTime) new Program()); изменен на Nullable<DateTime> nullable = new Nullable<DateTime>((DateTime) new Program());

Как вы можете видеть, преобразования implicit только компилятором превращаются в explicit.

Я предполагаю, что компилятор сначала считает [1] что вы не можете сделать Foo a DateTime?, так как a) это не ссылочный тип, а b ) это не a DateTime. Таким образом, он может проходить через некоторые из его неявных операторов [2] и находит тот, который соответствует DateTime. И затем он реализует [1] что вы можете преобразовать DateTime в Nullable<DateTime>, чтобы он это сделал.

:)

[1]: Не думает, но не педантична.
[2]: Эрик Липперт имеет комментарий об этом, что объясняет это.