Страницы

Поиск по вопросам

среда, 10 октября 2018 г.

Кастинг массива к IReadOnlyCollection

Почему нижеследующий код работает?
int[] ixs = new int[10]; int cnt = ((IReadOnlyCollection)ixs).Count;
Как это вообще компилируется? Ведь у массива нет свойства Count (у него есть Length и LongLength), соответственно - он не должен приводиться к IReadOnlyCollection


Ответ

Свойства Count у массива и правда нету. Но для того, чтобы имплементировать интерфейс, оно и не нужно.
Есть такая штука в .NET — explicit interface implementation. Это когда класс имплементирует интерфейс, но не заводит метод с нужным именем. (Она и применяется в случае массива.)
Пример. Пусть у нас есть такой интерфейс:
interface I { void Run(); }
Тогда мы можем сделать так:
class C : I { public void Run() { ... } }
А можем и так:
class C : I { void I.Run() { ... } }
При этом интерфейс имплементирован в обоих случаях, но в первом случае у нас есть публичный метод Run, а во втором нету, и этот метод можно вызвать только через каст к нужному интерфейсу.

Для чего такая фича языка (explicit interface implementation) реально нужна? А вот для чего. Представьте себе, что вы хотите реализовать в классе два интерфейса, которые конфликтуют между собой, определяют функцию или свойство с одинаковым именем, которые нужно для разных интерфейсов реализовывать по-разному. Тогда вы можете написать две разные имплементации:
class C : IExecutioner, IAsyncModelObject { Task IExecutioner.Execute() { /* казним приговорённого */ } Task IAsyncModelObject.Execute() { /* начинаем работу в фоне */ } }

Любой массив в .NET произведён от System.Array. Самой имплементации конкретных массивов в исходниках фреймворка нету, она генерируется автоматически. Документация говорит:
Starting with the .NET Framework 2.0, the Array class implements the System.Collections.Generic.IList, System.Collections.Generic.ICollection, and System.Collections.Generic.IEnumerable generic interfaces. The implementations are provided to arrays at run time, and as a result, the generic interfaces do not appear in the declaration syntax for the Array class.
В реальности, начиная с .NET 4.5, добавлены тем же механизмом ещё два интерфейса: System.Collections.Generic.IReadOnlyList and System.Collections.Generic.IReadOnlyCollection. Все эти интерфейсы реализует не класс Array, а производные от него классы, представляющие собой массивы конкретного типа.
Посмотрим интерфейсы, реализуемые типом System.Array и конкретным типом массива int[]. Такой код:
foreach (var i in typeof(System.Array).GetInterfaces()) Console.WriteLine(i);
выдаёт
System.ICloneable System.Collections.IList System.Collections.ICollection System.Collections.IEnumerable System.Collections.IStructuralComparable System.Collections.IStructuralEquatable
В то же время
foreach (var i in typeof(int[]).GetInterfaces()) Console.WriteLine(i);
выдаёт
System.ICloneable System.Collections.IList System.Collections.ICollection System.Collections.IEnumerable System.Collections.IStructuralComparable System.Collections.IStructuralEquatable System.Collections.Generic.IList`1[System.Int32] System.Collections.Generic.ICollection`1[System.Int32] System.Collections.Generic.IEnumerable`1[System.Int32] System.Collections.Generic.IReadOnlyList`1[System.Int32] System.Collections.Generic.IReadOnlyCollection`1[System.Int32]
Итак, лишь конкретные массивы имплементируют IReadOnlyCollection, сам System.Array этого не делает.

Окей, но откуда же берётся сам код свойства? Заглянем в исходники .NET (Array -- базовый класс всех массивов) видим:
public abstract class Array : ICloneable, IList, IStructuralComparable, IStructuralEquatable { // ... int ICollection.Count { get { return Length; } }
Таким образом, свойства Count действительно нет, есть лишь свойство ICollection.Count, которое доступно только через каст к интерфейсу. Это показывает, откуда взялась реализация ICollection, но что же насчёт IReadOnlyCollection? Как правильно отмечает @andreycha в соседнем ответе, для реализации generic-интерфейсов используется немного компиляторной магии и класс SZArrayHelper
Из исходников
// This class is needed to allow an SZ array of type T[] to expose IList, // IList, etc., etc. all the way up to IList. When the following call is // made: // // ((IList) (new U[n])).SomeIListMethod() // // the interface stub dispatcher treats this as a special case, loads up SZArrayHelper, // finds the corresponding generic method (matched simply by method name), instantiates // it for type and executes it.
Перевод:
// Этот класс (SZArrayHelper) нужен, чтобы позволить массиву T[] имплементировать // IList, IList и т. д. аж до IList. если вызывается // // ((IList) (new U[n])).МетодИзIList() // // код, отвечающий за нахождение интерфейсов в классе считает это особым случаем, // загружает SZArrayHelper, находит соответствующий обобщённый метод (просто по имени) // инстанциирует его для нужного типа T и выполняет.
Это значит, что каст работает, но код реально находится в SZArrayHelper, и то, что этот код используется, гарантируется специальными трюками рантайма. Реальное место нахождения свойства Count — SZArrayHelper.get_Count

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

Отправить комментарий