Подробнее о неявных операторах преобразования и интерфейсах в C # (снова) - PullRequest
23 голосов
/ 10 февраля 2012

Хорошо.Я прочитал этот пост, и я не совсем понимаю, как он применим к моему примеру (ниже).

class Foo
{
    public static implicit operator Foo(IFooCompatible fooLike)
    {
        return fooLike.ToFoo();
    }
}

interface IFooCompatible
{
    Foo ToFoo();
    void FromFoo(Foo foo);
}

class Bar : IFooCompatible
{
    public Foo ToFoo()
    {
        return new Foo();   
    }

    public void FromFoo(Foo foo)
    {
    }
}

class Program
{
    static void Main(string[] args)
    {
        Foo foo = new Bar();
        // should be the same as:
        // var foo = (new Bar()).ToFoo();
    }
}

Я полностью прочитал пост, на который я ссылался.Я прочитал раздел 10.10.3 спецификации C # 4.Все приведенные примеры относятся к дженерикам и наследованию, где вышеупомянутое не позволяет.

Может кто-нибудь объяснить, почему это не разрешено в контексте этого примера ?

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

Редактировать 1:

Я понимаю , что это не разрешено, потому что есть правила против этого.Я смущен относительно , почему это не разрешено.

Ответы [ 4 ]

41 голосов
/ 10 февраля 2012

Я понимаю, что это запрещено, потому что есть правила против этого. Я не понимаю, почему это не разрешено.

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

Вы не можете, например, выполнить пользовательское преобразование из MyClass в Object, поскольку уже является неявным преобразованием из MyClass в Object. «Встроенное» преобразование будет всегда выигрывать , поэтому бессмысленно объявлять пользовательское преобразование.

Более того, вы даже не можете сделать пользовательское неявное преобразование, которое заменяет встроенное явное преобразование. Вы не можете, например, сделать пользовательское неявное преобразование из Object в MyClass, потому что уже есть встроенное явное преобразование из Object в MyClass. Для читателя кода это слишком запутанно, чтобы позволить вам произвольно реклассифицировать существующие явные преобразования как неявные преобразования.

Это особенно случай, когда идентичность . Если я скажу:

object someObject = new MyClass();
MyClass myclass = (MyClass) someObject;

тогда я ожидаю, что это означает, что "someObject на самом деле имеет тип MyClass, это явное преобразование ссылок, и теперь myclass и someObject равны ссылкам". Если бы вам было позволено сказать

public static implicit operator MyClass(object o) { return new MyClass(); }

тогда

object someObject = new MyClass();
MyClass myclass = someObject;

будет допустимым , а два объекта не будут иметь ссылочного равенства , что странно .

У нас уже есть достаточно правил, чтобы дисквалифицировать ваш код, который преобразуется из интерфейса в открытый тип класса. Учтите следующее:

class Foo { }
class Foo2 : Foo, IBlah { }
...
IBlah blah = new Foo2();
Foo foo = (Foo) blah;

Это работает, и можно разумно ожидать, что blah и foo являются ссылочными равными, потому что приведение Foo2 к его базовому типу Foo не меняет ссылку. Теперь предположим, что это законно:

class Foo 
{
    public static implicit operator Foo(IBlah blah) { return new Foo(); }
}

Если это законно, то этот код является законным:

IBlah blah = new Foo2();
Foo foo = blah;

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

Таким образом, правило, что вы не должны заменять любое встроенное преобразование каким-либо определенным пользователем преобразованием, является достаточным , чтобы лишить вас возможности создавать преобразование, которое принимает интерфейс.

Но подождите! Предположим, что Foo является запечатанным . Тогда - это без преобразования между IBlah и Foo, явным или неявным, потому что не может быть возможно производным Foo2, который реализует IBlah. В этом случае мы должны разрешить пользовательское преобразование между Foo и IBlah? Такое пользовательское преобразование не может заменить любое встроенное преобразование, явное или неявное.

Нет. В разделе 10.10.3 спецификации добавлено дополнительное правило, которое явно запрещает любое пользовательское преобразование в интерфейс или из интерфейса независимо от того, заменяет ли оно встроенное преобразование или нет.

Почему? Поскольку есть разумное ожидание, что когда кто-то преобразует значение в интерфейс, то вы проверяете, реализует ли рассматриваемый объект интерфейс , а не , запрашивая совершенно другое объект, который реализует интерфейс. В терминах COM преобразование в интерфейс QueryInterface - " реализуете ли вы этот интерфейс? " - а не QueryService - " можете ли вы найти кого-то, кто реализует этот интерфейс?"

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

Таким образом, всегда незаконно делать пользовательское преобразование, которое преобразует в или изинтерфейс.

Тем не менее, генерики значительно мутят воду, формулировка спецификации не очень ясна, и компилятор C # содержит ряд ошибок в его реализации .Ни спецификация, ни реализация не являются правильными, учитывая определенные крайние случаи, связанные с дженериками, и это представляет сложную проблему для меня, для разработчика.Сегодня я работаю с Мэдсом над уточнением этого раздела спецификации, так как я реализую его в Рослине на следующей неделе.Я попытаюсь сделать это с минимальным количеством возможных изменений, но может потребоваться небольшое количество, чтобы привести поведение компилятора и язык спецификации в соответствие друг с другом.

22 голосов
/ 10 февраля 2012

В контексте вашего примера, он не будет работать снова, потому что неявный оператор был помещен в интерфейс ... Я не уверен, что вы думаете, что ваш пример отличается от того, который вы связали, кроме того, что вы пытаетесьпередать один конкретный тип другому через интерфейс.

Здесь обсуждается тема подключения:

http://connect.microsoft.com/VisualStudio/feedback/details/318122/allow-user-defined-implicit-type-conversion-to-interface-in-c

И Эрик Липперт, возможно, объяснилпричина, когда он сказал в вашем связанном вопросе:

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

Это , по-видимому, связано с идентификацией типов.Конкретные типы связаны друг с другом через их иерархию, так что идентификация типов может быть применена через нее.С интерфейсами (и других блокированных вещей, таких как dynamic и object) тип идентичности становится спорным, потому что любой / все могут быть размещены в таких типов.

1021 * Почему это важно, я понятия не имею.

Я предпочитаю явный код, который показывает, что я пытаюсь получить Foo от другого, IFooCompatible, поэтому процедура преобразования, которая принимает T where T : IFooCompatible, возвращает Foo.

По вашему вопросу я понимаю смысл обсуждения, однако мой остроумный ответ таков: если я увижу код типа Foo f = new Bar() в дикой природе, я, скорее всего, реорганизую его.


Альтернативное решение:

Не переусердствуйте с пудингом здесь:

Foo f = new Bar().ToFoo();

Вы уже высказали идею, что Foo совместимые типы реализуют интерфейс для достижения совместимости, используйте это в своемкод.


Литье по сравнению с преобразованием:

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

interface IFoo {}
class Foo : IFoo {}
class Bar : IFoo {}

Foo f = new Foo();
IFoo fInt = f;
Bar b = (Bar)fInt; // Fails.

Приведение понимает иерархию типов, и ссылка на fInt не может быть приведена кBar как это действительно Foo.Вы можете предоставить пользовательский оператор, который, возможно, предоставит следующее:

public static implicit operator Foo(Bar b) { };

И выполнение этого в вашем примере кода работает, но это начинает становиться глупым.

Преобразование, с другой стороны, полностью не зависит от иерархии типов.Его поведение совершенно произвольно - вы кодируете то, что хотите.Это тот случай, когда вы конвертируете Bar в Foo, вы просто помечаете конвертируемые предметы с IFooCompatible.Этот интерфейс не делает приведение законным к разным классам реализации.


Почему интерфейсы не разрешены в определяемых пользователем операторах преобразования:

Почему я не могу использовать интерфейс с явным оператором?

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

9 голосов
/ 10 февраля 2012

Хорошо, вот пример того, почему я считаю, что ограничение здесь:

class Foo
{
    public static implicit operator Foo(IFooCompatible fooLike)
    {
        return fooLike.ToFoo();
    }
}

class FooChild : Foo, IFooCompatible
{
}

...

Foo foo = new FooChild();
IFooCompatible ifoo = (IFooCompatible) foo;

Что должен делать компилятор и что должно происходить во время выполнения? foo уже относится к реализации IFooCompatible, поэтому с этой точки зрения он должен просто сделать это ссылочным преобразованием - но компилятор не знает , что это так Так должно ли оно на самом деле просто вызывать неявное преобразование?

I подозреваю основная логика: не позволяйте оператору определять, какой может конфликтовать с уже действительным преобразованием на основе типа времени выполнения. Хорошо, чтобы было ровно ноль или одно возможное преобразование из выражения в целевой тип.

(РЕДАКТИРОВАТЬ: ответ Адама звучит так, как будто он говорит об одном и том же - не стесняйтесь расценивать мой ответ как просто пример его:)

1 голос
/ 10 февраля 2012

Здесь, вероятно, было бы полезно, чтобы .net предоставил «чистый» способ связать интерфейс со статическим типом и иметь различные типы операций на типах интерфейса, сопоставленных с соответствующими операциями на статическом типе.В некоторых сценариях это может быть достигнуто с помощью методов расширения, но это уродливо и ограниченно.Связь с интерфейсами со статическими классами может предложить некоторые существенные преимущества:

  1. В настоящее время, если интерфейс желает предложить потребителям несколько перегрузок функции, каждая реализация должна реализовывать каждую перегрузку.Сопряжение статического класса с интерфейсом и разрешение этому классу объявлять методы в стиле методов расширения позволило бы потребителям класса использовать перегрузки, предоставляемые статическим классом, как если бы они были частью интерфейса, не требуя, чтобы разработчики предоставили их,Это можно сделать с помощью методов расширения, но для этого необходимо, чтобы статический метод был вручную импортирован на стороне потребителя.
  2. Есть много обстоятельств, когда интерфейс имеет некоторые статические методы или свойства, которые очень сильно связаны с ним (например, `Enumerable.Empty`).Возможность использовать одно и то же имя для интерфейса и «класс» связанных свойств может показаться более чистой, чем необходимость использовать отдельные имена для двух целей.
  3. Это обеспечит путь к поддержке необязательных членов интерфейса;если член существует в интерфейсе, но не в реализации, слот vtable может быть связан со статическим методом.Это была бы чрезвычайно полезная функция, поскольку она позволяла бы расширять интерфейсы без нарушения существующих реализаций.

Учитывая, что, к сожалению, такая возможность существует только в той степени, которая необходима для работы с COM-объектами, лучший альтернативный подход, который я могу представить, - это определить тип структуры, который содержит один член типа интерфейса и реализуетинтерфейс, выступая в качестве прокси.Преобразование из интерфейса в структуру не потребовало бы создания дополнительного объекта в куче, и если бы функции должны были обеспечивать перегрузки, которые приняли эту структуру, они могли бы преобразовать обратно в тип интерфейса таким способом, чей чистый результат был бы сохранением значенияи не требует бокса.К сожалению, передача такой структуры методу, который использует тип интерфейса, повлечет за собой бокс.Можно было бы ограничить глубину упаковки, если бы конструктор структуры проверил, был ли переданный ему объект типа интерфейса вложенным экземпляром этой структуры, и если это так, разверните один слой упаковки.Это может быть немного странно, но может быть полезно в некоторых случаях.

...