Эквивалентные неявные операторы: почему они законны? - PullRequest
6 голосов
/ 25 августа 2010

Обновление!

См. Мое описание части спецификации C # ниже; Я думаю, что я что-то упускаю, потому что для me похоже, что поведение, которое я описываю в этом вопросе, на самом деле нарушает спецификацию.

Обновление 2!

Хорошо, после дальнейших размышлений и на основании некоторых комментариев, я думаю, что теперь я понимаю, что происходит. Слова «тип источника» в спецификации относятся к типу, который преобразуется из - т.е. Type2 в моем примере ниже - это просто означает, что компилятор может сузить кандидатов до определены два оператора (поскольку Type2 является типом источника для обоих). Однако это не может сузить выбор дальше. Таким образом, ключевые слова в спецификации (как это относится к этому вопросу) «тип источника» , который я ранее неверно истолковал (я думаю) как «объявление типа».


Оригинальный вопрос

Допустим, у меня определены следующие типы:

class Type0
{
    public string Value { get; private set; }

    public Type0(string value)
    {
        Value = value;
    }
}

class Type1 : Type0
{
    public Type1(string value) : base(value) { }

    public static implicit operator Type1(Type2 other)
    {
        return new Type1("Converted using Type1's operator.");
    }
}

class Type2 : Type0
{
    public Type2(string value) : base(value) { }

    public static implicit operator Type1(Type2 other)
    {
        return new Type1("Converted using Type2's operator.");
    }
}

Тогда скажите, что я делаю это:

Type2 t2 = new Type2("B");
Type1 t1 = t2;

Очевидно, что это неоднозначно, так как не ясно, какой оператор implicit следует использовать. У меня вопрос: поскольку я не вижу какого-либо способа разрешить эту неоднозначность (я не могу выполнить какое-то явное приведение, чтобы уточнить, какую версию я хочу), и все же приведенные выше определения классов компилируются - - с какой стати компилятор вообще разрешает эти implicit операторы?


Вскрытие

Хорошо, я собираюсь пройтись по выдержке из спецификации C #, приведенной Гансом Пассантом, в попытке разобраться в этом.

Найдите множество типов D, из которых пользовательские операторы преобразования будут быть принятым во внимание. Этот набор состоит из S (если S является классом или структурой), база классы S (если S класс) и T (если T является классом или структурой).

Мы конвертируем из Type2 ( S ) в Type1 ( T ). Таким образом, кажется, что здесь D будет включать в себя все три типа в примере: Type0 (потому что это базовый класс S ), Type1 ( T *) 1064 *) и Type2 ( S ).

Найдите набор применимых определяемые пользователем операторы преобразования, U. Этот набор состоит из пользовательских объявлены операторы неявного преобразования по классам или структурам в D, что преобразовать из типа, охватывающего S в тип, охватываемый T. Если U пусто, преобразование не определено и происходит ошибка времени компиляции.

Хорошо, у нас есть два оператора, удовлетворяющих этим условиям. Версия, объявленная в Type1, отвечает требованиям, поскольку Type1 находится в D и преобразуется из Type2 (что, очевидно, охватывает S ) в Type1 (что, очевидно, охватывается T ). Версия в Type2 также соответствует требованиям по тем же причинам. Так что U включает в себя оба этих оператора.

Наконец, в отношении поиска наиболее конкретного «типа источника» SX операторов в U :

Если какой-либо из операторов в U преобразуется из S, то SX равен S.

Теперь оба оператора в U конвертируют из S - так что это говорит мне, что SX равно S .

Не означает ли это, что следует использовать версию Type2?

Но подождите! Я в замешательстве!

Не могу ли я только определить версию Type1 оператора, в этом случае единственным оставшимся кандидатом будет версия Type1, и все же в соответствии со спецификацией SX будет Type2? Это выглядит как возможный сценарий, в котором спецификация требует чего-то невозможного (а именно, что преобразование, объявленное в Type2, должно использоваться, когда на самом деле оно не существует).

Ответы [ 2 ]

2 голосов
/ 25 августа 2010

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

Есть преимущество в том, что мы не пытаемся запретить то, что нельзя запретить, поскольку это способствует ясности и последовательности (и в этом есть урок для законодателей).

1 голос
/ 25 августа 2010

Мы не хотим, чтобы это была ошибка времени компиляции, просто чтобы определить преобразования, которые могут вызвать двусмысленность. Предположим, что мы изменили Type0 для хранения double, и по какой-то причине мы хотим обеспечить отдельные преобразования в целое число со знаком и целое число без знака.

class Type0
{
    public double Value { get; private set; }

    public Type0(double value)
    {
        Value = value;
    }

    public static implicit operator Int32(Type0 other)
    {
        return (Int32)other.Value;
    }

    public static implicit operator UInt32(Type0 other)
    {
        return (UInt32)Math.Abs(other.Value);
    }

}

Это прекрасно компилируется, и я могу использовать оба преобразования с

Type0 t = new Type0(0.9);
int i = t;
UInt32 u = t;

Однако попытка float f = t является ошибкой компиляции, поскольку любое из неявных преобразований может использоваться для получения целочисленного типа, который затем может быть преобразован в число с плавающей точкой.

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

EDIT

Так как Ханс удалил свой ответ, который цитировал спецификацию, вот краткий обзор части спецификации C #, которая определяет, является ли преобразование неоднозначным, определив U как множество всех преобразований, которые могли бы выполнять работу:

  • Найдите наиболее конкретный тип источника, SX, из операторов в U:
    • Если любой из операторов в U конвертируется из S, то SX равен S.
    • В противном случае SX является наиболее охватываемым типом в объединенном наборе целевых типов операторов в U. Если не удается найти наиболее охватываемый тип, преобразование является неоднозначным, и возникает ошибка времени компиляции.

Перефразируя, мы предпочитаем преобразование, которое преобразует непосредственно из S, в противном случае мы предпочитаем тип, который является "самым простым" для преобразования S в. В обоих примерах у нас есть два преобразования из S. Если бы не было преобразований из Type2, мы бы предпочли преобразование из Type0 вместо одного из object. Если ни один тип не является лучшим выбором для конвертации, мы потерпим неудачу.

  • Найдите наиболее конкретный тип цели, TX, из операторов в U:
    • Если какой-либо из операторов в U преобразуется в T, то TX - это T.
    • В противном случае TX является наиболее охватывающим типом в объединенном наборе целевых типов операторов в U. Если не удается найти наиболее охватывающий тип, преобразование является неоднозначным, и возникает ошибка времени компиляции.

Опять же, мы бы предпочли преобразовать напрямую в T, но мы остановимся на типе, который «проще всего» преобразовать в T. В примере Дана у нас есть два доступных преобразования в T. В моем примере возможными целями являются Int32 и UInt32, и ни одно из них не является лучшим совпадением, чем другое, так что в этом случае преобразование завершается неудачно. Компилятор не может узнать, означает ли float f = t float f = (float)(Int32)t или float f = (float)(UInt32)t.

  • Если U содержит ровно один определяемый пользователем оператор преобразования, который преобразует из SX в TX, то это самый специфический оператор преобразования. Если такого оператора не существует или существует более одного такого оператора, преобразование является неоднозначным и возникает ошибка времени компиляции.

В примере Дэна мы терпим неудачу, потому что у нас осталось два преобразования из SX в TX. У нас не может быть преобразований из SX в TX, если мы выбираем разные преобразования при выборе SX и TX. Например, если бы у нас было Type1a, полученное из Type1, то мы могли бы иметь преобразования от Type2 до Type1a и от Type0 до Type1. Они все равно дали бы нам SX = Type2 и TX = Type1 , но на самом деле у нас нет никакого преобразования из Type2 в Type1. Это нормально, потому что это действительно неоднозначно. Компилятор не знает, нужно ли преобразовывать Type2 в Type1a и затем приводить к Type1 или сначала приводить к Type0, чтобы он мог использовать это преобразование в Type1.

...