Я заметил это при использовании выражений switch в C #; Я хотел бы знать, намеренно ли это или что-то совсем другое.

public interface IBase
{
}

public struct Base1: IBase
{
    //fields, methods, etc
}

public struct Base2: IBase
{
    //fields, methods, etc
}

public struct BaseContainer: IBase
{
    public IBase InnerBase{get;}

    public BaseContainer(IBase value)
    {
        //validation and other goodies...
        InnerBase = value;
    }

    public static implicit operator BaseContainer(Base1 value) => new BaseContainer(value);

    public static implicit operator BaseContainer(Base2 value) => new BaseContainer(value);
}

public class Util
{
    public static IBase NewBase(int somethingToSwitchOn)
    {
        return somethingToSwitchOn switch
        {
            1 => new Base1(),
            2 => new Base2(),
            _ => default(BaseContainer)
        };
    }
}

Вот где это становится странным:


public static void Main(string[] args)
{
    var @base = Util.NewBase(1);
    
    Console.WriteLine(@base.GetType().Name);
}

Приведенный выше код выводит «BaseContainer» вместо «Base1», как ожидалось, до тех пор, пока ЛЮБОЙ этап переключения не будет явно преобразован в IBase, например:

1 => new Base1(),
2 => (IBase) new Base2(),
_ => default(BaseContainer)
2
Dantte 10 Окт 2021 в 15:15

2 ответа

Лучший ответ

Компилятору необходимо определить тип выражения переключения. В конце концов, его тип не может быть одновременно Base1, Base2 и BaseContainer.

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

Tr M<X>(X x1 ... X xm)

С выражениями рук выражения switch, переданными в качестве аргументов.

Я не буду вдаваться в подробности вывода типов, но в целом это довольно интуитивно понятно (как здесь). Лучший общий тип для new Base1(), new Base2() и default(BaseContainer) - это BaseContainer, а лучший общий тип для new Base1(), (IBase)new Base2() и default(BaseContainer) равно IBase.

Так что, если вы не приведете приведение, выражение switch создаст BaseContainer, что означает, что созданный вами новый объект Base1 должен быть сначала преобразован в BaseContainer. Это вызывает ваш неявный оператор, который возвращает объект BaseContainer, и поэтому GetType возвращает BaseContainer.

Если вы бросите любую из рук на IBase, тогда все выражение switch произведет IBase. Преобразование из Base1 в IBase выполняется boxing conversion, и поэтому не меняет его динамический тип (то есть то, что возвращает GetType).

2
Sweeper 10 Окт 2021 в 12:59

Все это связано с выводом типа. Тот факт, что существует неявное преобразование как Base1, так и Base2 в BaseContainer, означает, что оно будет рассмотрено в первую очередь перед возможным преобразованием упаковки в интерфейс.

Мы видим, что спецификация языка говорит об этом

Поиск наиболее распространенного типа набора выражений

..snip .. Точнее, вывод начинается с переменной нефиксированного типа X. Выводы типа вывода затем делаются от каждого Ei до X. Наконец, X является фиксированным , и в случае успеха результирующий тип S становится лучшим общим типом для выражений. Если такого S не существует, выражения не имеют наилучшего общего типа.

Что исправляет?

< Сильный > Закрепление

Переменная нефиксированного типа Xi с набором границ фиксируется следующим образом:

  • Набор типов-кандидатов Uj начинается как набор всех типов в наборе границ для Xi.
  • Затем мы исследуем каждую границу для Xi по очереди: ..snip .. Для каждой нижней границы U Xi всех типов Uj, к которым нет неявного преобразования из U, удаляются из набора кандидатов. Для каждой верхней границы U Xi все типы Uj, из которых не было неявного преобразования в U, удаляются из набора кандидатов.
  • Если среди оставшихся типов-кандидатов Uj есть уникальный тип V, из которого происходит неявное преобразование во все другие типы-кандидаты, то Xi фиксируется на V.
2
Charlieface 10 Окт 2021 в 12:49