Недавно передо мной встала следующая задача: необходимо сравнить множество пар объектов. Но есть один нюанс: объекты — самые что ни на есть
object
'ы, а сравнивать нужно по всему набору публичных свойств. Причём совершенно необязательно, что типы сравниваемых объектов реализуют интерфейс IEquatable<T>
. Было очевидно, что следует использовать рефлексию. Однако при реализации я столкнулся со множеством тонкостей и в конечном счёте прибегнул к
Часть 1. Объект в отражении
«Подать сюдаMFCIEqualityComparer!», — кричал он, топая всеми четырьмя лапами
В .NET принято, что классы, выполняющие сравнение объектов, реализуют интерфейс IEqualityComparer<T>. Это позволяет внедрять их в классы коллекций, например, списки и словари, и использовать кастомную проверку равенства при поиске. Мы не будем отступать от этого соглашения и приступим к реализации интерфейса
IEqualityComparer<object>
, использующей механизм рефлексии. Напомним, что рефлексией называется инспектирование метаданных и скомпилированного кода в процессе выполнения программы (читателю, незнакомому с рефлексией, настоятельно рекомендуется ознакомиться с главой «Рефлексия и метаданные» книги Джозефа и Бена Албахари «C# 5.0. Справочник. Полное описание языка»). public class ReflectionComparer : IEqualityComparer<object>
{
public new bool Equals(object x, object y)
{
public new bool Equals(object x, object y)
{
return CompareObjectsInternal(x?.GetType(), x, y);
}
}
public int GetHashCode(object obj)
{
return obj.GetHashCode();
}
private bool CompareObjectsInternal(Type type, object x, object y)
{
throw new NotImplementedException();
}
}
Заметим, что метод
Equals
мы отметили как new
. Это сделано потому, что он перекрывает метод Object.Equals(object, object)
.Теперь сделаем шаг назад и точнее определим, что именно мы будем сравнивать. Объекты могут быть сколь угодно сложными, всё нужно учесть. Комбинируя следующие типы свойств мы сможем покрыть широкий класс входных данных:
- все примитивные типы (Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, IntPtr, UIntPtr, Char, Double, Single);
- строки;
- массивы (для простоты будем рассматривать только одномерные массивы);
- перечисления;
- коллекции, реализующие интерфейс IEnumerable<T>;
- структуры;
- классы.
Напишем каркас метода
CompareObjectsInternal
, а затем рассмотрим некоторые частные случаи.private bool CompareObjectsInternal(Type type, object x, object y)
{
// Если ссылки указывают на один и тот же объект
if (ReferenceEquals(x, y)) return true;
// Один из объектов равен null
if (ReferenceEquals(x, null) != ReferenceEquals(y, null)) return false;
// Объекты имеют разные типы
if (x.GetType() != y.GetType()) return false;
// Строки
if (Type.GetTypeCode(type) == TypeCode.String) return ((string)x).Equals((string)y);
// Массивы
if (type.IsArray) return CompareArrays(type, x, y);
// Коллекции
if (type.IsImplementIEnumerable()) return CompareEnumerables(type, x, y);
// Ссылочные типы
if (type.IsClass || type.IsInterface) return CompareAllProperties(type, x, y);
// Примитивные типы или типы перечислений
if (type.IsPrimitive || type.IsEnum) return x.Equals(y);
// Обнуляемые типы
if (type.IsNullable()) return CompareNullables(type, x, y);
// Структуры
if (type.IsValueType) return CompareAllProperties(type, x, y);
return x.Equals(y);
}
Приведённый выше код достаточно понятен: сначала проверяем объекты на ссылочное равенство и равенство
null
, затем проверяем типы, а после этого рассматриваем различные случаи. Причём для строк, примитивных типов и типов перечислений мы, не мудрствуя лукаво, вызываем метод Equals
. Для проверки типов на принадлежность к обнуляемым типам и типам коллекций мы используем методы расширения IsNullable
и IsImplementIEnumerable
, исходный код которых можно посмотреть ниже.public static bool IsImplementIEnumerable(this Type type) => type.GetInterface("IEnumerable`1") != null;
public static Type GetIEnumerableInterface(this Type type) => type.GetInterface("IEnumerable`1");
public static bool IsNullable(this Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof (Nullable<>);
В случае классов, интерфейсов и структур мы сравниваем все соответствующие свойства. Заметим, что индексаторы в .NET также представляют собой свойства, однако мы ограничимся простыми (parameterless) свойствами, имеющими геттеры, а сравнение коллекций будем обрабатывать отдельно. Проверить, является ли свойство индексатором мы можем с помощью метода PropertyInfo.GetIndexParameters. Если метод вернул массив ненулевой длины, значит мы имеем дело с индексатором.
private bool CompareAllProperties(Type type, object x, object y)
{
var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public);
var readableNonIndexers = properties.Where(p => p.CanRead && p.GetIndexParameters().Length == 0);
foreach (PropertyInfo propertyInfo in readableNonIndexers)
{
var a = propertyInfo.GetValue(x, null);
var b = propertyInfo.GetValue(y, null);
if (!CompareObjectsInternal(propertyInfo.PropertyType, a, b)) return false;
}
return true;
}
Следующий случае — сравнение объектов nullable-типов. Если нижележащий тип примитивный, то мы можем сравнить значения методом
Equals
. Если же внутри объектов лежат структуры, тогда необходимо сначала извлечь внутренние знаения, а затем сравнить их по всем публичным свойствам. Мы безопасно можем обращаться к свойству Value
, поскольку проверка на равенства уже была выполнена ранее в методе CompareObjectsInternal
. private bool CompareNullables(Type type, object x, object y)
{
Type underlyingTypeOfNullableType = Nullable.GetUnderlyingType(type);
if (underlyingTypeOfNullableType.IsPrimitive)
{
return x.Equals(y);
}
var valueProperty = type.GetProperty("Value");
var a = valueProperty.GetValue(x, null);
var b = valueProperty.GetValue(y, null);
return CompareAllProperties(underlyingTypeOfNullableType, a, b);
}
Перейдём к сравнению коллекций. Реализацию можно выполнить как минимум двумя способами: написать свой необобщённый метод, работающий с IEnumerable, либо использовать метод расширения Enumerable.SequenceEqual<TSource>. Для коллекций, элементами которых являются тип-значения, очевидно, что необобщённый метод будет работать медленее, т.к. ему постоянно придётся выполнять упаковку/распаковку значений. При использовании же метода LINQ нам нужно сначала подставить тип элементов коллекции в параметр-типа TSource с помощью метода MakeGenericMethod), а затем вызвать метод передав две сравниваемые коллекции. Причём, если коллекция содержит элементы непримитивных типов, то мы можем передать дополнительный аргумент — компарер — текущий экземпляр нашего класса
ReflectionComparer
(не зря же мы реализовывали интерфейс IEqualityComparer<object>
!). Поэтому для сравнения коллекций выбираем LINQ: private static MethodInfo GenericSequenceEqualWithoutComparer = typeof(Enumerable)
.GetMethods(BindingFlags.Public | BindingFlags.Static)
.First(m => m.Name == "SequenceEqual" && m.GetParameters().Length == 2);
private static MethodInfo GenericSequenceEqualWithComparer = typeof(Enumerable)
.GetMethods(BindingFlags.Public | BindingFlags.Static)
.First(m => m.Name == "SequenceEqual" && m.GetParameters().Length == 3);
private bool CompareEnumerables(Type collectionType, object x, object y)
{
Type enumerableInterface = collectionType.GetIEnumerableInterface();
Type elementType = enumerableInterface.GetGenericArguments()[0];
MethodInfo sequenceEqual;
object[] arguments;
if (elementType.IsPrimitive)
{
sequenceEqual = GenericSequenceEqualWithoutComparer;
arguments = new[] {x, y};
}
else
{
sequenceEqual = GenericSequenceEqualWithComparer;
arguments = new[] {x, y, this};
}
var sequenceEqualMethod = sequenceEqual.MakeGenericMethod(elementType);
return (bool)sequenceEqualMethod.Invoke(null, arguments);
}
Последний случай — сравнение массивов — во многом похож на сравнение коллекций. Отличие состоит в том, что вместо использования стандартного метода LINQ мы используем самописный обобщённый метод. Реализация сравнения массивов представлена ниже:
private static MethodInfo GenericCompareArraysMethod =
typeof(ReflectionComparer).GetMethod("GenericCompareArrays", BindingFlags.NonPublic | BindingFlags.Static);
private static bool GenericCompareArrays<T>(T[] x, T[] y, IEqualityComparer<T> comparer)
{
var comp = comparer ?? EqualityComparer<T>.Default;
for (int i = 0; i < x.Length; ++i)
{
if (!comp.Equals(x[i], y[i])) return false;
}
return true;
}
private bool CompareArrays(Type type, object x, object y)
{
var elementType = type.GetElementType();
int xLength, yLength;
if (elementType.IsValueType)
{
// Массивы типов-значений не приводятся к массиву object, поэтому используем Array
xLength = ((Array) x).Length;
yLength = ((Array) y).Length;
}
else
{
xLength = ((object[]) x).Length;
yLength = ((object[]) y).Length;
}
if (xLength != yLength) return false;
var compareArraysPrimitive = GenericCompareArraysMethod.MakeGenericMethod(elementType);
var arguments = elementType.IsPrimitive ? new[] {x, y, null} : new[] {x, y, this};
return (bool) compareArraysPrimitive.Invoke(null, arguments);
}
Итак, все части пазла собраны — наш рефлексивный компарер готов. Ниже представлены результаты сравнения производительности рефлексивного компарера по сравнению с ручной реализацией на «тестовых» данных. Очевидно, что время выполнения сравнения очень сильно зависит от типа сравниваемых объектов. В качестве примера здесь сравнивались объекты двух типов — условно «посложнее» и «попроще». Для оценки времени выполнения использовалась библиотека BenchmarkDotNet, за которую особую благодарность хочется выразить Андрею Акиньшину DreamWalker.
public struct Struct
{
private int m_a;
private double m_b;
private string m_c;
public int A => m_a;
public double B => m_b;
public string C => m_c;
public Struct(int a, double b, string c)
{
m_a = a;
m_b = b;
m_c = c;
}
}
public class SimpleClass
{
public int A { get; set; }
public Struct B { get; set; }
}
public class ComplexClass
{
public int A { get; set; }
public IntPtr B { get; set; }
public UIntPtr C { get; set; }
public string D { get; set; }
public SimpleClass E { get; set; }
public int? F { get; set; }
public int[] G { get; set; }
public List<int> H { get; set; }
public double I { get; set; }
public float J { get; set; }
}
[BenchmarkTask(platform: BenchmarkPlatform.X86, jitVersion: BenchmarkJitVersion.LegacyJit)]
[BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.LegacyJit)]
[BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.RyuJit)]
public class ComparisonTest
{
private static int[] MakeArray(int count)
{
var array = new int[count];
for (int i = 0; i < array.Length; ++i)
array[i] = i;
return array;
}
private static List<int> MakeList(int count)
{
var list = new List<int>(count);
for (int i = 0; i < list.Count; ++i)
list.Add(i);
return list;
}
private ComplexClass x = new ComplexClass
{
A = 2,
B = new IntPtr(2),
C = new UIntPtr(2),
D = "abc",
E = new SimpleClass { A = 42, B = new Struct(42, 3.14, "meow") },
F = 1,
G = MakeArray(100),
H = MakeList(100),
I = double.MaxValue,
J = float.MaxValue
};
private ComplexClass y = new ComplexClass
{
A = 2,
B = new IntPtr(2),
C = new UIntPtr(2),
D = "abc",
E = new SimpleClass { A = 42, B = new Struct(42, 3.14, "meow") },
F = 1,
G = MakeArray(100),
H = MakeList(100),
I = double.MaxValue,
J = float.MaxValue
};
private ReflectionComparer comparer = new ReflectionComparer();
[Benchmark]
public void ReflectionCompare()
{
var _ = comparer.Equals(x, y);
}
[Benchmark]
public void ManualCompare()
{
var _ = CompareComplexObjects();
}
private bool CompareComplexObjects()
{
if (x == y) return true;
if (x.A != y.A) return false;
if (x.B != y.B) return false;
if (x.C != y.C) return false;
if (x.D != y.D) return false;
if (x.E != y.E)
{
if (x.E.A != y.E.A) return false;
var s1 = x.E.B;
var s2 = y.E.B;
if (s1.A != s2.A) return false;
if (!s1.B.Equals(s2.B)) return false;
if (s1.C != s2.C) return false;
}
if (x.F != y.F) return false;
if (x.G != y.G)
{
if (x.G?.Length != y.G?.Length) return false;
int[] a = x.G, b = y.G;
for (int i = 0; i < a.Length; ++i)
{
if (a[i] != b[i]) return false;
}
}
if (x.H != y.H)
{
if (!x.H.SequenceEqual(y.H)) return false;
}
if (!x.I.Equals(y.I)) return false;
if (!x.J.Equals(y.J)) return false;
return true;
}
}
[BenchmarkTask(platform: BenchmarkPlatform.X86, jitVersion: BenchmarkJitVersion.LegacyJit)]
[BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.LegacyJit)]
[BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.RyuJit)]
public class SimpleComparisonTest
{
private SimpleClass x = new SimpleClass { A = 42, B = new Struct(42, 3.14, "meow") };
private SimpleClass y = new SimpleClass { A = 42, B = new Struct(42, 3.14, "meow") };
private ReflectionComparer comparer = new ReflectionComparer();
[Benchmark]
public void ReflectionCompare()
{
var _ = comparer.Equals(x, y);
}
[Benchmark]
public void ManualCompare()
{
var _ = CompareSimpleObjects();
}
private bool CompareSimpleObjects()
{
if (x == y) return true;
if (x.A != y.A) return false;
var s1 = x.B;
var s2 = y.B;
if (s1.A != s2.A) return false;
if (!s1.B.Equals(s2.B)) return false;
if (s1.C != s2.C) return false;
return true;
}
}
OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel Core(TM) i5-2410M CPU @ 2.30GHz, ProcessorCount=4
HostCLR=MS.NET 4.0.30319.42000, Arch=64-bit [RyuJIT]
Результаты сравнения объектов ComplexClass
Method | Platform | Jit | AvrTime | StdDev | op/s |
---|---|---|---|---|---|
ManualCompare | X64 | LegacyJit | 1,364.3835 ns | 47.6975 ns | 732,941.68 |
ReflectionCompare | X64 | LegacyJit | 36,779.9097 ns | 3,080.9738 ns | 27,188.92 |
ManualCompare | X64 | RyuJit | 930.8761 ns | 43.6018 ns | 1,074,294.12 |
ReflectionCompare | X64 | RyuJit | 36,909.7334 ns | 3,762.0698 ns | 27,093.98 |
ManualCompare | X86 | LegacyJit | 936.3367 ns | 38.3831 ns | 1,067,992.54 |
ReflectionCompare | X86 | LegacyJit | 32,446.6969 ns | 1,687.8442 ns | 30,819.81 |
Результаты сравнения объектов SimpleClass
Method | Platform | Jit | AvrTime | StdDev | op/s |
---|---|---|---|---|---|
Handwritten | X64 | LegacyJit | 131.5205 ns | 4.9045 ns | 7,603,376.64 |
ReflectionComparer | X64 | LegacyJit | 3,859.7102 ns | 269.8845 ns | 259,087.15 |
Handwritten | X64 | RyuJit | 61.2438 ns | 1.9025 ns | 16,328,222.24 |
ReflectionComparer | X64 | RyuJit | 3,841.4645 ns | 374.0006 ns | 260,317.46 |
Handwritten | X86 | LegacyJit | 71.5982 ns | 5.4304 ns | 13,966,823.95 |
ReflectionComparer | X86 | LegacyJit | 3,636.7963 ns | 241.3940 ns | 274,967.76 |
Естественно, прозводительность рефлексивного компарера ниже самописного. Рефлексия медленна и её API работает с
object
'ами, что незамедлительно сказывается на производительности, если в сравниваемых объектах присутствуют типы-значения, из-за необходимости выполнять boxing/unboxing. И тут уже необходимо исходить из собственных потребностей. Если рефлексия используется не часто, то с ней можно жить. Но в конкретном моём случае, рефлексивный компарер был недостаточно быстр. «Давай по новойЧасть 2. Игра в emitацию
Можно жить так, но лучше ускориться
Группа Ленинград, «Мне бы в небо»
Нео: И ты это читаешь?
Сайфер: Приходится. Со временем привыкаешь. Я даже не замечаю код. Я вижу блондинку, брюнетку, рыженькую.
Фильм «Матрица» (The Matrix)
Рефлексия позволяет нам извлечь всю необходимую информацию о типах сравниваемых объектов. Но эти типы мы не можем использовать напрямую для получения нужных нам свойств или вызова требуемого метода, что приводит нас к использованию медленного API рефлексии (PropertyInfo.GetValue, MethodInfo.Invoke и т.д.). А что, если бы мы могли, используя информацию о типах, единожды сгенерировать код сравнения объектов и вызывать его каждый раз, не прибегая более к рефлексии? И, к счастью, мы можем это сделать! Пространство имён System.Reflection.Emit предоставляет нам средства для создания динамических методов — DynamicMethod с помощью генератора IL — ILGenerator. Именно такой же подход используется, к примеру, для компиляции регулярных выражений в самом .NET Framework (реализацию можно посмотреть тут).
О генерации IL на Хабре уже писали. Поэтому лишь вкратце напомним об особенностях IL.
Intermediate Language (IL) представляет собой объектно-ориентированный язык ассемблера, используемый платформой .NET и Mono. Высокоуровневые языки, например, C#, VB.NET, F#, компилируются в IL, а IL в свою очередь компилируется JIT'ом в машинный код. Из-за своего промежуточного положения в этой цепочке язык и имеет своё название. IL использует стековую модель вычислений, то есть все входные и выходные данные передаются через стек, а не через регистры, как во многих процессорных архитектурах. IL поддерживает инструкции загрузки и сохранения локальных переменных и аргументов, преобразование типов, создание и манипулирование объектами, передачу управления, вызов методов, исключения и многие другие.
К примеру, инкрементация целочисленной переменной будет на IL записана следующим образом:
ldloc.0 // Загружаем переменную
ldc.i4.1 // Загружаем единицу
add // Складываем
stloc.0 // Сохраняем результат обратно в переменную
Посмотреть результат комплияции C# в IL можно посмотреть различными средствами, например, ILDasm (утилита, поставляемая вместе с Visual Studio) или ILSpy. Но мой любимый способ — это с помощью замечательного веб-приложения Try Roslyn, написанного Андреем Щёкиным ashmind.
Вернёмся к нашей задаче. Мы опять будем делать делать реализацию интерфейса
IEqualityComparer<object>
:public class DynamicCodeComparer : IEqualityComparer<object>
{
// Делегат для сравнения объектов
private delegate bool Comparer(object x, object y);
// Кэш сгенерированных компареров
private static Dictionary<Type, Comparer> ComparerCache = new Dictionary<Type, Comparer>();
public new bool Equals(object x, object y)
{
// Если ссылки указывают на один и тот же объект
if (ReferenceEquals(x, y)) return true;
// Один из объектов равен null
if (ReferenceEquals(x, null) != ReferenceEquals(y, null)) return false;
Type xType = x.GetType();
// Объекты имеют разные типы
if (xType != y.GetType()) return false;
//
// Проверяем наличие компарера в кэше. Если нет, то создаём его и сохраняем в кэш
//
Comparer comparer;
if (!ComparerCache.TryGetValue(xType, out comparer))
{
ComparerCache[xType] = comparer = new ComparerDelegateGenerator().Generate(xType);
}
return comparer(x, y);
}
public int GetHashCode(object obj)
{
return obj.GetHashCode();
}
}
Метод
Equals
использует словарь для поддержки кэша сгенерированных делегатов — компареров, выполняющих сравнение объектов. Если компарера для определённого типа ещё нет в словаре, то мы генерируем новый делегат. Вся логика по динамической генерации делегата будет сосредоточена в классе ComparerDelegateGenerator
. class ComparerDelegateGenerator
{
// Генератор кода динамического метода
private ILGenerator il;
public Comparer Generate(Type type)
{
// Создаём динамический метод в том же модуле, что и сравниваемый тип
var dynamicMethod = new DynamicMethod("__DynamicCompare", typeof(bool), new[] { typeof(object), typeof(object) },
type.Module);
il = dynamicMethod.GetILGenerator();
//
// Загружаем аргументы и прикастовываем их к типу времени выполнения
//
il.LoadFirstArg();
var arg0 = il.CastToType(type);
il.LoadSecondArg();
var arg1 = il.CastToType(type);
// Сравниваем объекты
CompareObjectsInternal(type, arg0, arg1);
// Если управление дошло до этого места, значит объекты равны
il.ReturnTrue();
// Создаём делегат для выполнения динамического метода
return (Comparer)dynamicMethod.CreateDelegate(typeof(Comparer));
}
}
В методе
Generate
выше есть один маленький нюанс. Нюанс состоит в том, чтобы создавать динамический метод в том же модуле, что и тип сравниваемых объектов. В противном случае код динамического метода не сможет получить доступ к типу и получит исключение TypeAccessException
. Класс ILGenerator
позволяет генерировать инструкции с помощью метода Emit(OpCode), принимающего в качестве аргумента код команды. Но, чтобы не засорять наш класс такими деталями мы будем использовать методы расширения, из названия которых будет понятно, что они делают. Код методов расширения LoadFirstArg, LoadSecondArg, CastToType
и ReturnTrue
представлен ниже. Следует пояснить, что вызываемый в Generate
метод CompareObjectsInternal
будет генерировать return false
сразу же, как только встретит отличающиеся значения. Поэтому последним операторам динамического метода будет return true
, чтобы обрабатать ту ситуацию, когда объекты равны. // Загружает в стек первый аргумент текущего метода
public static void LoadFirstArg(this ILGenerator il) => il.Emit(OpCodes.Ldarg_0);
// Загружает в стек второй аргумент текущего метода
public static void LoadSecondArg(this ILGenerator il) => il.Emit(OpCodes.Ldarg_1);
// Извлекает из стека значение и приводит его к заданному типу
public static LocalBuilder CastToType(this ILGenerator il, Type type)
{
var x = il.DeclareLocal(type);
// В случае типов-значений и примитивных типов выполняем распаковку
if (type.IsValueType || type.IsPrimitive)
{
il.Emit(OpCodes.Unbox_Any, type);
}
// В случае ссылочных типов выполняем приведение
else
{
il.Emit(OpCodes.Castclass, type);
}
il.SetLocal(x);
return x;
}
// Загружает в стек ноль (он же false)
public static void LoadZero(this ILGenerator il) => il.Emit(OpCodes.Ldc_I4_0);
// Загружает в стек единицу (она же true)
public static void LoadOne(this ILGenerator il) => il.Emit(OpCodes.Ldc_I4_1);
// Возвращает из метода значение false
public static void ReturnFalse(this ILGenerator il)
{
il.LoadZero();
il.Emit(OpCodes.Ret);
}
// Возвращает из метода значение true
public static void ReturnTrue(this ILGenerator il)
{
il.LoadOne();
il.Emit(OpCodes.Ret);
}
Далее рассмотрим метод
CompareObjectsInternal
, который будет генерировать различный код сравнения в зависимости от типа объектов:private void CompareObjectsInternal(Type type, LocalBuilder x, LocalBuilder y)
{
// Объявляем метку, на которую будем прыгать в случае, если объекты равны
var whenEqual = il.DefineLabel();
// Если объекты не являются типами-значений
if (!type.IsValueType)
{
// Тут же возвращаем true, если ссылки равны между собой
JumpIfReferenceEquals(x, y, whenEqual);
// Если один из объектов равен null, а второй нет возвращаем false
ReturnFalseIfOneIsNull(x, y);
// Массивы
if (type.IsArray)
{
CompareArrays(type.GetElementType(), x, y);
}
// Классы или интерфейсы
else if (type.IsClass || type.IsInterface)
{
// Строки
if (Type.GetTypeCode(type) == TypeCode.String)
{
CompareStrings(x, y, whenEqual);
}
// Коллекции
else if (type.IsImplementIEnumerable())
{
CompareEnumerables(type, x, y);
}
// Любые другие классы или интерфейсы
else
{
CompareAllProperties(type, x, y);
}
}
}
// Обнуляемые типы
else if (type.IsNullable())
{
CompareNullableValues(type, x, y, whenEqual);
}
// Примитивные типы или перечисления
else if (type.IsPrimitive || type.IsEnum)
{
ComparePrimitives(type, x, y, whenEqual);
}
// Структуры
else
{
CompareAllProperties(type, x, y);
}
// Ставим метку, на которую будем прыгать в случае, если объекты равны
il.MarkLabel(whenEqual);
}
Как видно из кода, мы обрабатываем те же ситуации, что и в рефлексивном компарере, но в немного другом порядке. Это связано с тем, что сравнение с
null
генерируется только для ссылочных типов, но не для обнуляемых. В случае обнуляемых объектов вместо этого генерируется проверка свойства HasValue
.Подробно разбирать каждый случай в статье не будем, в конце я дам ссылку на исходники. Но представим код сравнения массивов, чтобы прочувствовать, что из себя представляет разработка в условиях примитивных команд.
private void CompareArrays(Type elementType, LocalBuilder x, LocalBuilder y)
{
var loop = il.DefineLabel(); // Объявляем метку начала цикла сравнения элементов
il.LoadArrayLength(x); // Загружаем длину первого массива
il.LoadArrayLength(y); // Загружаем длину второго массива
il.JumpWhenEqual(loop); // Если длины равны, то переходим к циклу
il.ReturnFalse(); // Иначе возвращаем false
il.MarkLabel(loop); // Отмечаем метку начала цикла
var index = il.DeclareLocal(typeof(int)); // Объявляем счётчик цикла - индекс
var loopCondition = il.DefineLabel(); // Объявляем метку на проверку условия выхода из цикла
var loopBody = il.DefineLabel(); // Объявляем метку на тело цикла
il.LoadZero(); //
il.SetLocal(index); // Обнуляем индекс
il.Jump(loopCondition); // Прыгаем на проверку условия цикла
il.MarkLabel(loopBody); // Отмечаем начало тела цикла
{
var xElement = il.GetArrayElement(elementType, x, index); // Получаем элемент первого массива
var yElement = il.GetArrayElement(elementType, y, index); // Получаем элемент второго массива
CompareObjectsInternal(elementType, xElement, yElement); // Сравниваем элементы
il.Increment(index); // Увеличиваем счётчик
}
il.MarkLabel(loopCondition); // Отмечаем метку проверки условия выхода из цикла
{
il.LoadLocal(index); // Загружаем текущее значение индекса
il.LoadArrayLength(x); // Загружаем длину массива
il.JumpWhenLess(loopBody); // Если индекс не вышел за пределы диапазона, то прыгаем в тело цикла
}
}
Как мы уже отмечали выше, чтобы не
CompareArrays
сравнивает длины массив и переходит к дальнейшему сравнению только в том случае, если они равны. Далее выполняется цикл поэлементного сравнения массивов, но так как в IL нет таких высокоуровневых операторов, как циклы, то мы определяем в нашем коде несколько базовых блоков: инициализацию счётчика, тело и проверку условия выхода из цикла. Переход же между этими блоками осуществляется с помощью инструкций условного перехода (Jump, JumpWhenLess
) на метки loopBody, loopCondition
.// Загружает в стек значение заданной переменной
public static void LoadLocal(this ILGenerator il, LocalBuilder x) => il.Emit(OpCodes.Ldloc, x);
// Извлекает из стека значение и присваивает его заданной переменной
public static void SetLocal(this ILGenerator il, LocalBuilder x) => il.Emit(OpCodes.Stloc, x);
// Загружает в стек длину заданного массива
public static void LoadArrayLength(this ILGenerator il, LocalBuilder array)
{
il.LoadLocal(array);
il.Emit(OpCodes.Ldlen);
il.Emit(OpCodes.Conv_I4);
}
// Извлекает из стека массив и индекса, а загружает в стек элемент массива с заданным индексом
public static void LoadArrayElement(this ILGenerator il, Type type)
{
if (type.IsEnum)
{
type = Enum.GetUnderlyingType(type);
}
if (type.IsPrimitive)
{
if (type == typeof (IntPtr) || type == typeof (UIntPtr))
{
il.Emit(OpCodes.Ldelem_I);
}
else
{
OpCode opCode;
switch (Type.GetTypeCode(type))
{
case TypeCode.Boolean:
case TypeCode.Int32:
opCode = OpCodes.Ldelem_I4;
break;
case TypeCode.Char:
case TypeCode.UInt16:
opCode = OpCodes.Ldelem_U2;
break;
case TypeCode.SByte:
opCode = OpCodes.Ldelem_I1;
break;
case TypeCode.Byte:
opCode = OpCodes.Ldelem_U1;
break;
case TypeCode.Int16:
opCode = OpCodes.Ldelem_I2;
break;
case TypeCode.UInt32:
opCode = OpCodes.Ldelem_U4;
break;
case TypeCode.Int64:
case TypeCode.UInt64:
opCode = OpCodes.Ldelem_I8;
break;
case TypeCode.Single:
opCode = OpCodes.Ldelem_R4;
break;
case TypeCode.Double:
opCode = OpCodes.Ldelem_R8;
break;
default:
throw new ArgumentOutOfRangeException();
}
il.Emit(opCode);
}
}
else if (type.IsValueType)
{
il.Emit(OpCodes.Ldelema, type);
}
else
{
il.Emit(OpCodes.Ldelem_Ref);
}
}
// Возвращает новую переменную, содержащую элемент массива с заданным индексом
public static LocalBuilder GetArrayElement(this ILGenerator il, Type elementType, LocalBuilder array, LocalBuilder index)
{
var x = il.DeclareLocal(elementType);
il.LoadLocal(array);
il.LoadLocal(index);
il.LoadArrayElement(elementType);
il.SetLocal(x);
return x;
}
// Увеличивает значение заданной переменной на единицу
public static void Increment(this ILGenerator il, LocalBuilder x)
{
il.LoadLocal(x);
il.LoadOne();
il.Emit(OpCodes.Add);
il.SetLocal(x);
}
Попробуем применить
DynamicCodeComparer
к типу SimpleClass
, который мы использовали для сравнения прозводительности рефлексивного компарера:.method public static
bool __DynamicCompare (
object '',
object ''
) cil managed
{
// Method begins at RVA 0x2050
// Code size 215 (0xd7)
.maxstack 15
.locals init (
[0] class SimpleClass,
[1] class SimpleClass,
[2] int32,
[3] int32,
[4] valuetype Struct,
[5] valuetype Struct,
[6] int32,
[7] int32,
[8] float64,
[9] float64,
[10] string,
[11] string
)
IL_0000: ldarg.0
IL_0001: castclass SimpleClass
IL_0006: stloc.0
IL_0007: ldarg.1
IL_0008: castclass SimpleClass
IL_000d: stloc.1
IL_000e: ldloc.0
IL_000f: ldloc.1
IL_0010: beq IL_00d5
IL_0015: ldloc.0
IL_0016: ldnull
IL_0017: ceq
IL_0019: ldloc.1
IL_001a: ldnull
IL_001b: ceq
IL_001d: beq IL_0024
IL_0022: ldc.i4.0
IL_0023: ret
IL_0024: ldloc.0
IL_0025: callvirt instance int32 SimpleClass::get_A()
IL_002a: stloc.2
IL_002b: ldloc.1
IL_002c: callvirt instance int32 SimpleClass::get_A()
IL_0031: stloc.3
IL_0032: ldloc.2
IL_0033: ldloc.3
IL_0034: beq IL_003b
IL_0039: ldc.i4.0
IL_003a: ret
IL_003b: ldloc.0
IL_003c: callvirt instance valuetype Struct SimpleClass::get_B()
IL_0041: stloc.s 4
IL_0043: ldloc.1
IL_0044: callvirt instance valuetype Struct SimpleClass::get_B()
IL_0049: stloc.s 5
IL_004b: ldloca.s 4
IL_004d: call instance int32 Struct::get_A()
IL_0052: stloc.s 6
IL_0054: ldloca.s 5
IL_0056: call instance int32 Struct::get_A()
IL_005b: stloc.s 7
IL_005d: ldloc.s 6
IL_005f: ldloc.s 7
IL_0061: beq IL_0068
IL_0066: ldc.i4.0
IL_0067: ret
IL_0068: ldloca.s 4
IL_006a: call instance float64 Struct::get_B()
IL_006f: stloc.s 8
IL_0071: ldloca.s 5
IL_0073: call instance float64 Struct::get_B()
IL_0078: stloc.s 9
IL_007a: ldloc.s 8
IL_007c: call bool [mscorlib]System.Double::IsNaN(float64)
IL_0081: ldloc.s 9
IL_0083: call bool [mscorlib]System.Double::IsNaN(float64)
IL_0088: and
IL_0089: brtrue IL_0099
IL_008e: ldloc.s 8
IL_0090: ldloc.s 9
IL_0092: beq IL_0099
IL_0097: ldc.i4.0
IL_0098: ret
IL_0099: ldloca.s 4
IL_009b: call instance string Struct::get_C()
IL_00a0: stloc.s 10
IL_00a2: ldloca.s 5
IL_00a4: call instance string Struct::get_C()
IL_00a9: stloc.s 11
IL_00ab: ldloc.s 10
IL_00ad: ldloc.s 11
IL_00af: beq IL_00d5
IL_00b4: ldloc.s 10
IL_00b6: ldnull
IL_00b7: ceq
IL_00b9: ldloc.s 11
IL_00bb: ldnull
IL_00bc: ceq
IL_00be: beq IL_00c5
IL_00c3: ldc.i4.0
IL_00c4: ret
IL_00c5: ldloc.s 10
IL_00c7: ldloc.s 11
IL_00c9: call instance bool [mscorlib]System.String::Equals(string)
IL_00ce: brtrue IL_00d5
IL_00d3: ldc.i4.0
IL_00d4: ret
IL_00d5: ldc.i4.1
IL_00d6: ret
} // end of method Test::__DynamicCompare
public static bool __DynamicCompare(object obj, object obj2)
{
SimpleClass simpleClass = (SimpleClass)obj;
SimpleClass simpleClass2 = (SimpleClass)obj2;
if (simpleClass != simpleClass2)
{
if (simpleClass == null != (simpleClass2 == null))
{
return false;
}
int a = simpleClass.A;
int a2 = simpleClass2.A;
if (a != a2)
{
return false;
}
Struct b = simpleClass.B;
Struct b2 = simpleClass2.B;
int a3 = b.get_A();
int a4 = b2.get_A();
if (a3 != a4)
{
return false;
}
double b3 = b.get_B();
double b4 = b2.get_B();
if (!(double.IsNaN(b3) & double.IsNaN(b4)) && b3 != b4)
{
return false;
}
string c = b.get_C();
string c2 = b2.get_C();
if (c != c2)
{
if (c == null != (c2 == null))
{
return false;
}
if (!c.Equals(c2))
{
return false;
}
}
}
return true;
}
Декомпилированный метод выглядит достаточно понятно, чтобы проверить, что он корректно выполняет сравнение. Единственное к чему можно было бы придраться, так это обилие временных переменных, но это, как говорится, издержки производства.
Результаты
Ниже представлены результаты сравнения производительности
DynamicCodeComparer, ReflectionComparer
и написанного вручную сравнения на тех же входных данных, на которых мы проводили наш микробенчмарк для рефлексивного компарера. Как видно генерация IL позволяет получить гораздо более эффективную реализацию по сравнению с рефлексией.public struct Struct
{
private int m_a;
private double m_b;
private string m_c;
public int A => m_a;
public double B => m_b;
public string C => m_c;
public Struct(int a, double b, string c)
{
m_a = a;
m_b = b;
m_c = c;
}
}
public class SimpleClass
{
public int A { get; set; }
public Struct B { get; set; }
}
public class ComplexClass
{
public int A { get; set; }
public IntPtr B { get; set; }
public UIntPtr C { get; set; }
public string D { get; set; }
public SimpleClass E { get; set; }
public int? F { get; set; }
public int[] G { get; set; }
public List<int> H { get; set; }
public double I { get; set; }
public float J { get; set; }
}
[BenchmarkTask(platform: BenchmarkPlatform.X86, jitVersion: BenchmarkJitVersion.LegacyJit)]
[BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.LegacyJit)]
[BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.RyuJit)]
public class ComplexComparisonTest
{
private static int[] MakeArray(int count)
{
var array = new int[count];
for (int i = 0; i < array.Length; ++i)
array[i] = i;
return array;
}
private static List<int> MakeList(int count)
{
var list = new List<int>(count);
for (int i = 0; i < list.Count; ++i)
list.Add(i);
return list;
}
private ComplexClass x = new ComplexClass
{
A = 2,
B = new IntPtr(2),
C = new UIntPtr(2),
D = "Habrahabr!",
E = new SimpleClass { A = 42, B = new Struct(42, 3.14, "meow") },
F = 1,
G = MakeArray(100),
H = MakeList(100),
I = double.MaxValue,
J = float.MaxValue
};
private ComplexClass y = new ComplexClass
{
A = 2,
B = new IntPtr(2),
C = new UIntPtr(2),
D = "Habrahabr!",
E = new SimpleClass { A = 42, B = new Struct(42, 3.14, "meow") },
F = 1,
G = MakeArray(100),
H = MakeList(100),
I = double.MaxValue,
J = float.MaxValue
};
private ReflectionComparer reflectionComparer = new ReflectionComparer();
private DynamicCodeComparer dynamicCodeComparer = new DynamicCodeComparer();
[Benchmark]
public void ReflectionCompare()
{
var _ = reflectionComparer.Equals(x, y);
}
[Benchmark]
public void DynamicCodeCompare()
{
var _ = dynamicCodeComparer.Equals(x, y);
}
[Benchmark]
public void ManualCompare()
{
var _ = CompareComplexObjects();
}
private bool CompareComplexObjects()
{
if (x == y) return true;
if (x.A != y.A) return false;
if (x.B != y.B) return false;
if (x.C != y.C) return false;
if (x.D != y.D) return false;
if (x.E != y.E)
{
if (x.E.A != y.E.A) return false;
var s1 = x.E.B;
var s2 = y.E.B;
if (s1.A != s2.A) return false;
if (!s1.B.Equals(s2.B)) return false;
if (s1.C != s2.C) return false;
}
if (x.F != y.F) return false;
if (x.G != y.G)
{
if (x.G?.Length != y.G?.Length) return false;
int[] a = x.G, b = y.G;
for (int i = 0; i < a.Length; ++i)
{
if (a[i] != b[i]) return false;
}
}
if (x.H != y.H)
{
if (!x.H.SequenceEqual(y.H)) return false;
}
if (!x.I.Equals(y.I)) return false;
if (!x.J.Equals(y.J)) return false;
return true;
}
}
[BenchmarkTask(platform: BenchmarkPlatform.X86, jitVersion: BenchmarkJitVersion.LegacyJit)]
[BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.LegacyJit)]
[BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.RyuJit)]
public class SimpleComparisonTest
{
private SimpleClass x = new SimpleClass { A = 42, B = new Struct(42, 3.14, "meow") };
private SimpleClass y = new SimpleClass { A = 42, B = new Struct(42, 3.14, "meow") };
private ReflectionComparer reflectionComparer = new ReflectionComparer();
private DynamicCodeComparer dynamicCodeComparer = new DynamicCodeComparer();
[Benchmark]
public void ReflectionCompare()
{
var _ = reflectionComparer.Equals(x, y);
}
[Benchmark]
public void DynamicCodeCompare()
{
var _ = dynamicCodeComparer.Equals(x, y);
}
[Benchmark]
public void ManualCompare()
{
var _ = CompareSimpleObjects();
}
private bool CompareSimpleObjects()
{
if (x == y) return true;
if (x.A != y.A) return false;
var s1 = x.B;
var s2 = y.B;
if (s1.A != s2.A) return false;
if (!s1.B.Equals(s2.B)) return false;
if (s1.C != s2.C) return false;
return true;
}
}
Результаты сравнения объектов ComplexClass
Method | Platform | Jit | AvrTime | StdDev | op/s |
---|---|---|---|---|---|
DynamicCodeComparer | X64 | LegacyJit | 1,104.7155 ns | 32.9474 ns | 905,210.51 |
Handwritten | X64 | LegacyJit | 1,360.3273 ns | 39.9703 ns | 735,117.32 |
ReflectionComparer | X64 | LegacyJit | 38,043.3600 ns | 2,261.3159 ns | 26,290.11 |
DynamicCodeComparer | X64 | RyuJit | 834.8742 ns | 58.1986 ns | 1,197,785.93 |
Handwritten | X64 | RyuJit | 968.3789 ns | 33.1622 ns | 1,032,653.82 |
ReflectionComparer | X64 | RyuJit | 37,751.3104 ns | 1,763.3172 ns | 26,489.20 |
DynamicCodeComparer | X86 | LegacyJit | 776.0265 ns | 22.8038 ns | 1,288,615.79 |
Handwritten | X86 | LegacyJit | 915.5713 ns | 26.0536 ns | 1,092,214.32 |
ReflectionComparer | X86 | LegacyJit | 32,382.2746 ns | 1,748.4016 ns | 30,881.10 |
Результаты сравнения объектов SimpleClass
Method | Platform | Jit | AvrTime | StdDev | op/s |
---|---|---|---|---|---|
DynamicCodeComparer | X64 | LegacyJit | 215.7626 ns | 8.2063 ns | 4,634,725.08 |
Handwritten | X64 | LegacyJit | 160.4945 ns | 6.8949 ns | 6,230,741.94 |
ReflectionComparer | X64 | LegacyJit | 6,654.3290 ns | 380.7790 ns | 150,278.15 |
DynamicCodeComparer | X64 | RyuJit | 168.4194 ns | 9.4654 ns | 5,937,569.56 |
Handwritten | X64 | RyuJit | 87.8513 ns | 3.3118 ns | 11,382,874.20 |
ReflectionComparer | X64 | RyuJit | 6,954.6437 ns | 387.1803 ns | 143,789.85 |
DynamicCodeComparer | X86 | LegacyJit | 180.4105 ns | 6.5036 ns | 5,542,914.59 |
Handwritten | X86 | LegacyJit | 93.0846 ns | 4.0584 ns | 10,742,923.17 |
ReflectionComparer | X86 | LegacyJit | 6,431.5783 ns | 314.5633 ns | 155,483.09 |
Заключение
Фред Брукс в своей знаменитой статье «No silver bullet» подчёркивает разницу между ненужными случайными сложностями (accidental complexity) и имманентными сложностями (essential complexity), внутренне присущими самой решаемой задаче. Использование рефлексии зачастую является примером такой ненужной сложности, возникшей из-за того, что в какой-то момент дизайну программной системы не было уделено достаточно внимания. Поэтому прежде чем кидаться скорее использовать рефексию или кодогенерацию проверьте, может быть ещё не поздно подкорректировать проектное решение. Конкретно в моём случае сравнение могло было бы выполняться одной строчкой, если бы все сравниваемые объекты реализовывали бы интерфейс
IEquatable<T>
. Тем не менее я получил решение, которое возможно кому-нибудь ещё окажется полезным.Засим откланиваюсь, дорогие читатели. Приятного программирования!
Полезные ресурсы:
Комментарии (20)
Scratch
28.10.2015 20:24А можно таким макаром заменить в рантайме метод из какого-либо класса? Например, метод, который что-то проверяет и возвращает True, а мы раз и делаем False
forcewake
28.10.2015 23:08+1Можно. Гуглить по .NET CLR Injection, например вменяемая статья с примерами кода codeproject
Хотелось бы предупредить, что, если целитесь на подмену метода, проверяющего наличие лицензии, реализованного, скажем, с помощью LicFileLicenseProvider, то может не сработать.
Schrodingers_Cat
28.10.2015 23:34+4Такая задачка будет посложнее. Нужно знать структуру внутренних объектов CLR. Один из подходов к перехвату .NET'овских методов, про который я читал ещё давно вот тут состоит в том, чтобы подменять адрес JIT'овской заглушки целевого метода на метод-перехватчик. Но работать это будет только если хук поставить до JIT-компиляции. Со сборками, по которым прошлись NGEN'ом поэтому такой трюк не прокатит. Статья эта 2009 года, с тех пор многие структуры могли измениться. Другой подход, описанный тут, основан на использовании CLR Profiler API. Эта апишка используется и в Visual Studio для реализации edit-and-continue, то есть для применения правок к коду прямо во время отладки. И ещё есть готовый продукт — библиотека CodeCop, которая позволяет перехватывать метод до его выполнения, после его выполнения, или вообще целиком подменить метод. Бесплатная версия позволяет установить максимум 25 хуков, а за полный функционал разработчики просят денежку. Как реализован CodeCop ничего не могу сказать — репозиторий на BitBucket естественно закрытый. Но предполагаю, что они используют именно CLR Profiler API, потому что с RyeJit они пока не умеют дружить, как говорится здесь, а в блоге команды .NET как раз было упоминание, что при включении нового JIT'а функция Edit & Continue перестаёт работать.
shai_hulud
29.10.2015 12:55+1Советую для этих целей easyhook.github.io
Очень удобное API и возможно хукать всё что движется.
mird
29.10.2015 09:03Скажите, а что за прикладная задача требует подобного сравнения объектов?
Schrodingers_Cat
29.10.2015 10:27Объекты из моей задачи представляют собой параметры запуска различных измерительных тестов. Поведения у таких объектов нет никакого, они являются просто-напросто контейнерами свойств. Для каждого теста известны типовые (дефолтные) параметры запуска. Так вот в UI требуется по-разному отображать тесты с дефолтными и недефолтными параметрами.
mird
29.10.2015 12:38ммм. А вам не кажется, что гораздо проще было бы добавить одно единственное свойство сигнализирующее что параметры не дефолтные и заполнять его при изменении параметров в интерфейсе, чем городить заметную кучу рефлекшн кода?
Schrodingers_Cat
29.10.2015 13:14Проще-проще, Вы правы. Но к сожалению, добавить такое свойство не представлялось возможным (организационно). Если бы можно было изменить все эти классы, то я бы реализовал для них всех интерфейс
IEquatable<T>
. Как я написал в статье, это как раз пример accidental complexity.
mird
29.10.2015 09:09И еще. Вы сравниваете два ссылочных типа по свойствам. А вас не смущает, что для них может быть специальным образом переопределен метод equals, который определяет, что два объекта равны, если у них равны не все свойства, а только часть? В результате ваш метод вернет что объекты не равны, а они вообще-то равны.
Schrodingers_Cat
29.10.2015 11:00В моей задаче было достаточно именно такого сравнения, поскольку сравниваемые объекты, как я ответил выше, моделируют параметры запуска тестов и являются контейнерами свойств без всякого поведения. Но Ваше замечание справедливо и в реализации ещё много чего можно улучшить. Помимо вызова переопределённого метода
Equals
, было бы неплохо настраивать способ сравнения строк (StringComparison
) и макс. допустимое для сравненияdouble/float
. Плюс коллекции, которые реализуютIList<T>
(илиIReadOnlyList<T>
), возможно, что было бы более эффективно сравнивать не с помощьюEnumerable.SequenceEqual
, а циклом for. Кроме того в текущей реализацииDynamicCodeComparer
не учитывается, что структуры тоже могут быть коллекциями, а, например, в Roslyn такое сплошь и рядом, взять хотя быSeparatedSyntaxList<TNode>
. А ещё мне самому не очень нравится название класса DynamicCodeComparer. Задачу необходимо было решить в сжатые сроки, поэтому действительно не всё ещё доведено до ума.Sioln
29.10.2015 11:31Если бы не нужна была скорость выполнения, а время программиста стоило денег, то я бы предложил сериализовать объекты, например, в JSON и просто сравнивать строки.
mird
29.10.2015 12:34Вообще говоря, нет гарантии, что свойства будут сериализованы всегда в одном и том же порядке.
Sioln
29.10.2015 15:39Согласен.
Ещё есть вариант кодогенерации в буквальном смысле. На основании данных рефлексии генерируется код c#, скармливается csc и потом подгружается сборка.
Не так быстро, конечно, но зато вообще не нужно изучать такой низкий уровень.
mird
29.10.2015 12:40И в ту же копилку, у вас нет никакой (обработки) циклических ссылок. (Раз уж мы говорим об универсальном решении).
Ogi
29.10.2015 10:44+3
Теперь мне покажут мультик?Schrodingers_Cat
29.10.2015 12:06В контакте кстати одна девушка пишет тут, что на картинке как минимум 16 отличий: «из неочевидных: зеркальное отображение и угол наклона».
shai_hulud
Неплохо. Но гораздо удобнее генерировать динамический код через System.Linq.Expressions, в 4.5 помимо функциональщины добавили еще немного императивщины.
Schrodingers_Cat
Абсолютно согласен. Конечно у Reflection.Emit больше возможностей, но гораздо проще ошибиться и словить VerificationException. Пожалуй, что на досуге я добавлю ещё одну реализацию, использующую деревья выражений. Хотя опуститься на уровень IL было интересно, много нового для себя открыл.