Автор: Sergey Teplyakov

Поскольку мне говорят (точнее пишут), что я немного утомил с философией программирования и стоит браться за ум вспомнить и о технологиях, то я решил не откладывать это дело в долгий ящик, и опубликовать несколько заметок о языке C#. Тем более, что после подготовки к Hot Code остались некоторые наработки, которыми будет интересно поделиться.

Итак, у нас есть структура BigDouble с оператором неявного приведения типов к double и список этих структур:struct BigDouble
{
private readonly double _value,
public BigDouble(double value) { _value = value, }
public static implicit operator double(BigDouble value)
{
return value._value, }
}
var bigDoubles = new List
<,BigDouble>,
{
new BigDouble(42.0),
new BigDouble(18.0),
},

Вопрос #1. Будет ли работать следующий цикл foreach?
foreach (double d in bigDoubles)
{
Console.WriteLine(d),
}
Вопрос #2. Что мы получим в следующих случаях?
var query = from double d in bigDoubles
select d,
foreach (var d in query) { Console.WriteLine(d), }
Ответ #1

При описании особенностей цикла foreach я как-то упустил один интересный момент. Мы обсудили, что 'утиное' поведение цикла foreach обусловлено тем, что в языке C# 1.0 не было обобщений (generics), а использование лишь интерфейса IEnumerable/IEnumerator приводило бы к упаковке на каждой итерации цикла.

Но есть еще одна особенность цикла foreach, корни которой уходят в эту же эпоху: возможность приведения типов переменной цикла. Так, при работе со слаботипизированой коллекцией, типа ArrayList, мы могли написать так:ArrayList al = new ArrayList(),
al.Add(
'foo'),
foreach (string s in al) { Console.WriteLine(s), }

Эта возможность придавала работе с нетипизированными коллекциями некоторую форму строгой типизации. Можно подумать, что эта возможность работает только для приведения вверх/вниз по иерархии наследования (up/down casts), но поскольку в языке C# нет специализированных средств для такого приведения типов (нет аналога конструкции dynamic_cast языка C++), то это преобразование реализовано с помощью обычного преобразования типов (псевдокод):using (var enumerator = collection.GetEnumerator())
{
while (enumerator.MoveNext())
{
// Переменная цикла объявляется внутри каждой итерации
// только в C# 5.0!
T current = (T)enumerator.Current,
// Тело цикла!!
}
}

(где T тип переменной цикла foreach).

А раз так, то в цикле foreach можно использовать пользовательские преобразования типов, в результате код, приведенный в первом вопросе, будет успешно компилироваться и исполняться.

Ответ #2

LINQ запрос вида:var query = from double d in bigDoubles
select d,

Преобразуется в вызов метода Enumerable.Select:var query = bigDoubles.Cast<,double>,().Select(d =>, d),

И при выполнении любой из этих запросов упадет InvalidCastException. Причина в следующем. Enumerable.Cast является методом расширения интерфейса IEnumerable (а не IEnumerable<,T>,), а это значит, что внутри идет попытка преобразования не BigDouble ->, Nullable<,double>,, а упакованного объекта BigDouble, т.е. мы пытаемся преобразовать (object)BigDouble ->, double.

Как мы знаем (а если не знаем, то можем узнать в статье 'Распаковка и InvalidCastException'), мы не можем менять тип упакованного объекта при распаковке, поэтому даже попытка приведения упакованного int к long 'падает' с InvalidCastException, и даже следующий код завершается с исключением: Enumerable.Range(1, 10).Cast<,long>,().ToList(),

Решений этой проблемы несколько. Самый простой способ это добавить явное преобразование типа с помощью Enumerable.Select:foreach (double d in bigDoubles.Select(bd =>, (double)bd))
{ }

Или же можно сделать пару собственных методов расширения, которые будут выполнять необходимое преобразование внутри. Но это не так просто сделать, как кажется и наивная реализация следующего вида работать не будет:public static IEnumerable<,TTo>, NaiveCast<,TFrom, TTo>,(
this IEnumerable<,TFrom>, enumerable)
{
// Используем возможность цикла foreach
// 'изменять' тип переменной цикла
foreach (TTo value in
enumerable)
{
yield return value,
}
}

Проблема в этом случае в том, что типы TFrom и TTo с точки зрения компилятора никак не связаны, поэтому такое преобразование в общем случае невозможно. Мы можем добавить ограничение where TFrom : TTo, но и это не поможет, поскольку это ограничение определяет отношение наследования между типами TFrom и TTo, но не учитывает возможность пользовательских преобразований и мы не сможем преобразовать список BigDouble к double.

Вместо этого, мы должны создать обобщенный метод, который будет 'генерировать' код преобразования типов во время исполнения. Тут тоже есть несколько вариантов. Первый вариант это использование dynamic (метод DynamicCast), второй деревьев выражений (ExpressionCast):public static class EnumerableEx
{
public static IEnumerable<,TTo>, DynamicCast<,TFrom, TTo>,(
this IEnumerable<,TFrom>, enumerable)
{
foreach (TFrom current in enumerable)
{
yield return (TTo)((dynamic)current),
}
}
public static IEnumerable<,TTo>, ExpressionCast<,TFrom, TTo>,(
this IEnumerable<,TFrom>, enumerable)
{
foreach (var current in enumerable)
{
TTo cur =
DynamicConverter<,TFrom, TTo>,.Convert(current),
yield return cur,
}
}
}
internal static class DynamicConverter
<,TFrom, TTo>,
{
// Если генерация конвертора упадет с исключением,
// то вместо TypeLoadException мы получим оригинальное исключение
private static readonly Lazy<,Func
<,TFrom, TTo>,>, _converter =
new Lazy<,Func<,TFrom, TTo>,>,(GenerateConverter),
public static TTo Convert(TFrom valueToConvert)
{
return _converter.Value(valueToConvert),
}
private static Func<,TFrom, TTo>, GenerateConverter()
{
// Генерируем лямбда-выражение вида
// Func<,TFrom, TTo>, fun = (TFrom value) =>, (TTo)value,
var param = Expression.Parameter(typeof(TFrom)),
Expression convertExpr = Expression.Convert(
param,
typeof(TTo)),
var fun = Expression.Lambda<,Func<,TFrom, TTo>,>,(convertExpr, param).Compile(),
return fun,
}
}

После чего мы сможем преобразовать последовательность одного типа в последовательность другого типа (хотя в продакшне я все равно предпочел бы увидеть простой вызов метода Select):bigDoubles.ExpressionCast<,BigDouble, double>,(),

Об опасностях неявного преобразования

Структура похожая на BigDouble использовалась в одном из реальных проектов, но она содержала пару неявных преобразований: к Nullable<,double>, и из Nullable<,double>,, в результате использования которой возникла небольшая проблема.

Предположим, у нас есть следующая вью-модель (ради наглядности я привожу и модифицированный код структуры BigDouble):struct BigDouble
{
private readonly double? _value,
public BigDouble(double? value) { _value = value, }
public static implicit operator double?(BigDouble value)
{
return value._value,
}
public static implicit operator BigDouble(double? value)
{
return new BigDouble(value),
}
}
class CustomViewModel
{
public BigDouble Value1 { get, set, }
public BigDouble Value2 { get, set, }
public BigDouble Total { get { return Value1 + Value2, } }
}

Где-то в коде мы получаем список наших вью-моделей и фильтруем их следующим образом:var viewModels = new List<,CustomViewModel>,{
new CustomViewModel {Value1 = new BigDouble(42.0)},
new CustomViewModel {Value2 = new BigDouble(12.0)},},
var filteredVMs = viewModels.Where(vm =>, vm.Total >, 0).ToList(),

При этом автор этого кода рассчитывал увидеть в результирующем списке обе вью-модели. А сколько он получит в результате?

...

(да, filteredVMs будет пустым. Насколько понятно, почему?).

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

Дополнительные ссылки
  • Duck Typing или так ли прост старина foreach?
  • Распаковка и InvalidCastException

Помогла статья? Оцените её!
0 из 5. Общее количество голосов - 0
 

You have no rights to post comments

Дмитрий Крикунов

Публикую статьи, обучающие курсы и новости по программированию: алгоритмам, языкам (С++, Java), параллельному программированию, паттернам и библиотекам (Qt, boost).