Подтвердить что ты не робот

Подробнее о неявных операциях преобразования и интерфейсах в С# (опять же)

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

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 спецификации С# 4. Все приведенные примеры относятся к дженерикам и наследованию, где это не указано.

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

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

Изменить 1:

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

4b9b3361

Ответ 1

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

Здесь обсуждается тема:

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

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

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

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

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

Я предпочитаю явный код, который показывает мне. Я пытаюсь получить 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.

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

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

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

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


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

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

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

Ответ 2

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

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

Вы не можете, например, сделать пользовательское преобразование от 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 - "можете ли вы найти меня тем, кто реализует этот интерфейс?"

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

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

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

Ответ 3

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

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, поэтому с этой точки зрения он должен просто сделать это ссылочным преобразованием, - но компилятор не знает этого случая, поэтому действительно ли он должен просто вызвать неявное преобразование?

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

(EDIT: ответ Адама звучит так, будто он говорит примерно в одном и том же - не стесняйтесь считать мой ответ просто примером его:)

Ответ 4

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

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

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