четверг, 27 января 2011 г.

Почему следует избегать специализации обобщений для базовых классов и интерфейсов в C#

За этой статьей стоит одна бессонная ночь, проведенная в холодном дебагере Вижуалстудии. Я совершил ошибку. Интересную ошибку, которой спешу поделиться.

Речь пойдет об обобщениях в C#, их конкретизации и о том, что иногда не стоит доверять ни компилятору, ни собственному интеллекту, а просто не наследовать от мухи слона, заводя огромное количество ненужных абстракций.

Длинная вводная



Для начала, давайте определимся с понятиями.

Под Обобщениями я подразумеваю общие (или параметризованные) типы в языке C#. В англоязычной литературе они называются Generics. Более подробно про обобщения можно прочитать тут: Обобщения (Глава из книги “Язык программирования C# 2005 (Си Шарп) и платформа .NET 2.0”)

Под специализацией обобщения, чтобы не начинать словоблудие, я понимаю вот это:

static void PrintAllItems<T>(T collection) where T:IEnumerable
{
// foreach работает медленно, зато универсально
foreach (var item in collection)
{
Console.WriteLine("IEnumerable: " + item.ToString());
}
}

static void PrintAllItems(int[] intCollection)
{
// for работает быстрее foreach
for (int i = 0; i < intCollection.Length; i++ )
{
Console.WriteLine("int: " + intCollection[i]);
}
}
Метод PrintAllItems(T collection) - это обобщенный метод.
Метод PrintAllItems(int[] intCollection) – это специализация обобщеного метода.

И теперь, метод PrintAllItems будет работать для всех IEnumerable – типов одинаково, кроме типа int[], для которого метод будет работать немного иначе (хотя, int[] точно также реализует IEnumerable как и double):

static void Main(string[] args)
{
double[] doubleArray = { 1.1, 2.2, 3.3};
int[] intArray = { 1, 2, 3 };

PrintAllItems(doubleArray);
PrintAllItems(intArray);
}


Результат:
IEnumerable: 1.1
IEnumerable: 2.2
IEnumerable: 3.3
int: 1
int: 2
int: 3

И наконец-то, почему же таки следует избегать конкретизации обобщений для базовых классов и интерфейсов
Чем проще писать программисту код, используя такие синтаксические сладости, как перегрузки методов и обобщения, тем сложнее компилятору разрешать эти самые перегрузки. Ведь практически каждый обобщенный метод может соответствовать любому типу. Может получиться так, что приложение будет работать очень странно. Тут все зависит от вашей осторожности… или неосторожности. Именно вы (и я в том числе), ответственны за создание методов, которые, не должны… должны создавать минимальное, замешательство для программиста, использующего ваш код и для вас самих.
Будьте осторожны. Ваши обобщенные методы могут быть более обобщение того, чего от них ожидают.
Да, ваши обобщенные методы могут быть неожиданно универсальны, и поверьте, что это не всегда хорошо.

Давайте рассмотрим следующий пример. А пока вы читаете код, постарайтесь угадать, что и в какой последовательности он выведет на экран.


using System;
namespace ConsoleApplication6
{
public class MyAnimalBase
{
}
public interface ISayableAnimal
{
void Say();
}
public class MyCatDerived : MyAnimalBase, ISayableAnimal
{
#region ISayableAnimal Members
void ISayableAnimal.Say()
{
Console.WriteLine("Inside MyCatDerived.Say");
}
#endregion
}
public class AnotherCow : ISayableAnimal
{
#region ISayableAnimal Members
public void Say()
{
Console.WriteLine("Inside AnotherCow.Say");
}
#endregion
}
class Program
{
static void WriteMessage(MyAnimalBase b)
{
Console.WriteLine("Inside Say(MyAnimalBase)");
}
static void WriteMessage<T>(T obj)
{
Console.Write("Inside Say<T>(T): ");
Console.WriteLine(obj.ToString());
}
static void WriteMessage(ISayableAnimal obj)
{
Console.Write(
"Inside Say(ISayableAnimal): ");
obj.Say();
}
static void Main(string[] args)
{
MyCatDerived d = new MyCatDerived();
Console.WriteLine("01 Calling Program.Say");
WriteMessage(d);

Console.WriteLine();
Console.WriteLine(
"02 Calling through ISayableAnimal interface");
WriteMessage((ISayableAnimal)d);

Console.WriteLine();
Console.WriteLine("03 Cast to base object");
WriteMessage((MyAnimalBase)d);

Console.WriteLine();
Console.WriteLine("04 Another Type test:");
AnotherCow anObject = new AnotherCow();
WriteMessage(anObject);

Console.WriteLine();
Console.WriteLine("05 Cast to ISayableAnimal:");
WriteMessage((ISayableAnimal)anObject);
}
}
}

И вывод будет следующим:
01 Calling Program.Say
Inside Say(T): ConsoleApplication6.MyCatDerived

02 Calling through ISayableAnimal interface
Inside Say(ISayableAnimal): Inside MyCatDerived.Say

03 Cast to base object
Inside Say(MyAnimalBase)

04 Another Type test:
Inside Say(T): ConsoleApplication6.AnotherCow

05 Cast to ISayableAnimal:
Inside Say(ISayableAnimal): Inside AnotherCow.Say

По себе знаю, уважаемый читатель, что вы, скорее всего не бросились анализировать скучный код, сопоставляя его с результатом выполнения.
Давайте разберемся вместе, что же все-таки произошло.
Первый тест показывает, и это очень важно запомнить, то, что WriteMessage(T obj) лучше соответствует типу, чем WriteMessage(MyAnimalBase b), для объекта, который унаследован от MyAnimalBase. Это потому, что компилятор идет по пути наименьшего сопротивления, и создает метод, который абсолютно соответствует типу MyCatDerived, превращая T в MyCatDerived.
А вот метод WriteMessage(MyAnimalBase b) требует преобразования типа. Это сложнее для компилятора, а значит -- WriteMessage(T obj) лучше.

Но, давайте отойдем от абстрактных примеров с животными и используем пример с IQueryable, который, как вам известно наследуется IEnumerable. Как вы думаете, победит ли Say(IEnumerable enumerable) хотя бы раз?




static void Say<T>(T obj)
{
Console.WriteLine("Say<T> won!!!");
}

static void Say(IEnumerable enumerable)
{
Console.WriteLine("IEnumerable won!!!");
}

static void Main(string[] args)
{
int[] a = {1, 2, 3};

IQueryable q = a.AsQueryable();

Say(a);
Say(q);
}


Нет. Не победит.
Say won!!!
Say won!!!


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

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

А победа на войне – за нами!

По мотивам книги «More Effective C# 50 Specific Ways to Improve Your C#»
Глава: Item 7. Do Not Create Generic Specialization on Base Classes or
Interfaces

1 коммент.:

Анонимный комментирует...

카지노 카지노 m88 m88 クイーンカジノ クイーンカジノ 12bet 12bet 바카라사이트 바카라사이트 dafabet dafabet rb88 rb88 クイーンカジノ クイーンカジノ 902

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

 

.NET ate my MOSK;. Powered By Blogger © 2009 Bombeli | Theme Design: ooruc