Почему существует ограничение на явное приведение общего к типу класса, но нет ограничений на приведение общего к типу интерфейса?

#c# #generics

#c# #обобщения

Вопрос:

Читая документацию Microsoft, я наткнулся на такой интересный пример кода:

 interface ISomeInterface
{...}
class SomeClass
{...}
class MyClass<T> 
{
   void SomeMethod(T t)
   {
      ISomeInterface obj1 = (ISomeInterface)t;//Compiles
      SomeClass      obj2 = (SomeClass)t;     //Does not compile
   }
}
 

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

КСТАТИ — есть способ обойти ошибку компиляции, но это не устраняет логический беспорядок в моей голове:

 class MyOtherClass
{...}

class MyClass<T> 
{

   void SomeMethod(T t)

   {
      object temp = t;
      MyOtherClass obj = (MyOtherClass)temp;

   }
}
 

Комментарии:

1. Из любопытства: можете ли вы ввести «SomeClass obj2 = (SomeClass)(object)t;»?

2. да, это делается вторым фрагментом

3. Проверьте это philipm.at/2011/1014 , нашел это, когда пытался найти объяснение. Предупреждение о спойлере — может сбить вас с толку еще больше!;)

Ответ №1:

Это именно то, что вы получаете в обычных обстоятельствах — без обобщений — при попытке приведения между классами без отношения наследования:

  public interface IA
 {
 }

 public class B
 {
 }

 public class C
 {
 }

 public void SomeMethod( B b )
 {
     IA o1 = (IA) b;   <-- will compile
     C o2 = (C)b;  <-- won't compile
 }
 

Таким образом, без ограничения универсальный класс будет вести себя так, как будто между классами нет никакой связи.

Продолжение…

Ну, допустим, кто-то делает это:

  public class D : B, IA
 {
 }
 

И затем вызывает:

 SomeMethod( new D() );
 

Теперь вы поймете, почему компилятор позволяет передавать приведение интерфейса. Он действительно не может знать во время компиляции, реализован ли интерфейс или нет.

Помните, что общий класс вполне может быть написан кем-то, кто использует вашу сборку — спустя годы после того, как вы ее скомпилировали. Таким образом, нет никаких шансов, что компилятор может отказаться от его компиляции. Это должно быть проверено во время выполнения.

Комментарии:

1. хороший момент, но все еще непонятно, почему они решили разрешить одно приведение и запретить второе?

2. Обновил мой ответ, чтобы включить это.

3. Если бы вы попробовали D o3 = (D)b; , SomeMethod(B b) это скомпилировалось бы? Мне кажется, что применяется тот же аргумент, что то, что вы передаете, может быть потомком, который действителен для приведения, но может и не быть … (и да, я ленив и не просто пытаюсь это сделать сам в данный момент).

4. Да, ленивый @Chris 😉 это скомпилировалось бы.

5. «Он действительно не может знать во время компиляции, реализован ли интерфейс или нет». — почему? В нем есть вся информация, необходимая для вывода, также невозможно получить аргумент «годы назад». Компилятор откажется компилировать вашу новую библиотеку, а не старую

Ответ №2:

Большая разница в том, что интерфейс гарантированно будет ссылочным типом. Типы значений являются источником проблем. Это явно упоминается в спецификации языка C #, глава 6.2.6, с отличным примером, демонстрирующим проблему:


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

 class X<T>
{
    public static long F(T t) {
        return (long)t;             // Error 
    }
}
 

Если бы прямое явное преобразование t в int было разрешено, можно было бы легко ожидать, что Xf(7) вернет 7L . Однако этого не будет, потому что стандартные числовые преобразования рассматриваются только тогда, когда известно, что типы являются числовыми во время компиляции. Чтобы прояснить семантику, приведенный выше пример должен быть написан:

 class X<T>
{
    public static long F(T t) {
        return (long)(object)t;     // Ok, but will only work when T is long
    }
}
 

Теперь этот код будет компилироваться, но выполнение X.F(7) затем вызовет исключение во время выполнения, поскольку упакованный int не может быть преобразован непосредственно в long .

Ответ №3:

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

 // Compiles
ISomeInterface obj1 = (ISomeInterface)t;

// Сompiles too!
SomeClass obj2 = (SomeClass)(object)t;     
 

будет выдавать те же ошибки во время выполнения.

Таким образом, причина может быть в следующем: компилятор не знает, какие интерфейсы реализует класс, но он знает наследование классов (следовательно (SomeClass)(object)t , метод работает). Другими словами: недопустимое приведение запрещено в CLR, разница лишь в том, что в некоторых случаях его можно обнаружить во время компиляции, а в некоторых — нет. Основная причина этого в том, что даже если компилятор знает об интерфейсах всех классов, он не знает о своих потомках, которые могут его реализовать, и действительны для существования T . Рассмотрим следующий сценарий:

 namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            MyClass<SomeClass> mc = new MyClass<SomeClass>();

            mc.SomeMethod(new SomeClassNested());

        }
    }

    public interface ISomeInterface
    {
    }

    public class SomeClass
    {

    }

    public class SomeClassNested : SomeClass, ISomeInterface
    {

    }

    public class MyClass<T>
    {
        public void SomeMethod(T t)
        {
            // Compiles, no errors at runtime
            ISomeInterface obj1 = (ISomeInterface)t;
        }
    }
}
 

Комментарии:

1. как ты думаешь, почему он не знает? все написано в сборке в таблицах метаданных.

2. @Karel ну, все есть в сборке, это точно, но это не значит, что компилятор это знает. Компилятор — относительно «тупая» вещь.

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

4. @PetrAbdulin: Ах, я думаю, что теперь я понимаю, что ты имеешь в виду, да.

Ответ №4:

Я думаю, что разница между приведением к интерфейсу и приведением к классу заключается в том, что c # поддерживает множественное «наследование» только для интерфейсов. Что это значит? Компилятор может определить только во время компиляции, допустимо ли приведение для класса, потому что C # не допускает множественного наследования для классов.

С другой стороны, компилятор не знает во время компиляции, реализует ли ваш класс интерфейс, используемый в приведении. Почему? Кто-то может наследовать от вашего класса и реализовать интерфейс, используемый в вашем приведении. Итак, компилятор не знает об этом во время компиляции. (См SomeMethod4() . Ниже).

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

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

 interface ISomeInterface
{}
class SomeClass
{}

sealed class SealedClass
{
}

class OtherClass
{
}

class DerivedClass : SomeClass, ISomeInterface
{
}

class MyClass
{
  void OtherMethod(SomeClass s)
  {
    ISomeInterface t = (ISomeInterface)s; // Compiles!
  }

  void OtherMethod2(SealedClass sc)
  {
    ISomeInterface t = (ISomeInterface)sc; // Does not compile!
  }

  void OtherMethod3(SomeClass c)
  {
    OtherClass oc = (OtherClass)c; // Does not compile because compiler knows 
  }                                // that SomeClass does not inherit from OtherClass!

  void OtherMethod4()
  {
    OtherMethod(new DerivedClass()); // In this case the cast to ISomeInterface inside
  }                                  // the OtherMethod is valid!
}
 

То же самое верно и для обобщений.

Надеюсь, это поможет.