Рассмотрим один прием, помогающий выводить типы (на авторство не претендую) на примере мемоизации. В двух словах, если кто не вкурсе, мемоизация – это техника оптимизации (подвид кэширования), используемая для избежания вызовов функций с повторяющимися аргументами. Множество примеров реализации мемоизации на C# можно найти здесь. Оттолкнемся от мемоизации функции одного аргумента, описанного Wes Dyer-ом, одним из разработчиков компилятора C#.
public static Func<TKey, TResult> Memoize<TKey, TResult>(this Func<TKey, TResult> func)
{
if (func == null)
throw new ArgumentNullException("func");
var cache = new Dictionary<TKey, TResult>();
TResult result;
return key => (cache.TryGetValue(key, out result))
? result
: (cache[key] = func(key));
}Мой вариант отличается только тем, что я переменную result внес в замыкание вместо того, чтобы объявить ее внутри тела возвращаемой функции. Сделал я это только для того, чтобы не писать фигурные скобки в определении тела функции.
Все красиво и наглядно, но допустим возникла необходимость мемоизации функции двух переменных.
Мемоизация функции двух аргументов
Решать можно по-разному. Например, модифицировать реализацию мемоизации функции одной переменной, т.е. фактически переписать ее заново:public static Func<T1, T2, TResult> Memoize<T1, T2, TResult>(this Func<T1, T2, TResult> func)
{
if (func == null)
throw new ArgumentNullException("func");
var cache = new object[0].ToDictionary(
x => new { k1 = default(T1), k2 = default(T2) },
x => default(TResult));
return (k1, k2) =>
{
TResult result;
var key = new { k1, k2 };
return (cache.TryGetValue(key, out result))
? result
: (cache[key] = func(k1, k2));
};
}Здесь я воспользовался тем, что анонимный тип, который генерируется компилятором обладает всем необходимым для того, чтобы использовать его в качестве ключа словаря. Небольшая сложность заключалась в том, чтобы объявить переменную словаря с анонимным типом ключа, но выручил метод Enumerable.ToDictionary.
Но душа захотела реюза описанного ранее метода мемоизации функции одного аргумента. Мне пока известно два способа сделать реюз:
- Применить последовательно мемоизацию к каждому аргументу, что привлечет к появлению множества экземпляров словарей (не интересно для дальнейшего обсуждения)
- Применить мемоизацию к методу, который принимает аргумент-комбинацию двух аргументов, а возвращает результат вызова метода func, полученный по соответствующей паре аргументов. Далее будем обсуждать метод этот вариант.
Func<?, TResult> func2 = (? x) => func(x.t1, x.t2);Проблема здесь в том, что комбинированный аргумент представлен анонимным типом, указать который проблематично. Стандартных Tuple-ов нет, да и неизвестно, будут ли они обладать теми же свойствами, что и анонимные типы, будет ли возможность использовать их в качестве ключа словаря?
Итого, есть метод с сигнатурой Func<?, TResult>, записать тело которого мы можем, но не можем сказать компилятору, что за тип кроется за знаком “?” (достаточно было бы указать тип хотя бы в одном из мест, отмеченных знаком “?” в определении анонимного метода).
Вот тут-то и выручит нас метод, который ничего не делает:static Func<T, TResult> MakeFunc<T, TResult>(T _, Func<T, TResult> func)
{
return func;
}Метод MakeFunc абсолютно бесполезен во времени выполнения. Однако он помогает вывести тип “?”. Записать этот тип в коде мы все равно не сможем, но это и не требуется.
Вот как компилятор теперь может выявить тип метода: первым аргументом MakeFunc мы указываем экземпляр анонимного типа, а т.к. тип T первого аргумента совпадает с типом аргумента метода Func<T, TResult> второго аргумента метода MakeFunc, то компилятор теперь знает, что подразумевается под типом T в выражении Func<T, TResult>.
Теперь следующее выражениеvar func2 = MakeFunc(
new { A1 = default(T1), A2 = default(T2) },
x => func(x.A1, x.A2));в точности указывает компилятору, что func2 это метод, принимающий аргумент анонимного типа, и имеющий тело, которое возвращает TResult, обращаясь к оригинальному методу func, растаскивая анонимный тип на аргументы. Теперь к методу func2 можно применить мемоизацию:
public static Func<T1, T2, TResult> Memoize<T1, T2, TResult>(this Func<T1, T2, TResult> func)
{
if (func == null)
throw new ArgumentNullException("func");
var func2 = MakeFunc(
new { A1 = default(T1), A2 = default(T2) },
x => func(x.A1, x.A2));
var func2m = func2.Memoize();
return (t1, t2) => func2m(new { A1 = t1, A2 = t2 });
}Здесь находится топик обсуждения мемоизации функции двух аргументов на RSDN (автор – я). Обратите внимание, как легко и изящно решается проблема на языке F#.
Вышеописанный метод производит мемоизацию функции аргумента анонимного типа, затем возвращает метод, комбинирующий аргументы в анонимный тип, и передающий экземпляр анонимного типа в мемоизированную функцию.
Вообще говоря, я не думаю, что кому-то из читателей пригодится мемоизация функции двух и более аргументов… Пост немного о другом (как следует из его названия). Пост о том, как можно помочь компилятору вывести необходимый тип, в котором каким-то образом участвуют анонимные типы.Потому еще один пример, где описанный мной прием может пригодитсья:
Вывод типа IEqualityComparer<?> для анонимного типа.
Преамбула проста: когда-то не так давно я решил, что если не заморачиваться на эффективности, то можно не писать классы компареров каждый раз, а создать адаптер, который бы принимал функции GetHashCode и Equals и представлял бы их экземпляром IEqualityComparer<T>. Выглядит это чудо так:public static IEqualityComparer<T> CreateAdapter<T>(
Func<T, T, bool> equals,
Func<T, int> getHashCode)
{
if (equals == null)
throw new ArgumentNullException("equals");
if (getHashCode == null)
throw new ArgumentNullException("getHashCode");
return new EqualityComparerAdapter<T>(equals, getHashCode);
}
class EqualityComparerAdapter<T> : IEqualityComparer<T>
{
readonly Func<T, T, bool> _equals;
readonly Func<T, int> _getHashCode;
public EqualityComparerAdapter(Func<T, T, bool> equals, Func<T, int> getHashCode)
{
_equals = equals;
_getHashCode = getHashCode;
}
public bool Equals(T x, T y)
{
return _equals(x, y);
}
public int GetHashCode(T obj)
{
return _getHashCode(obj);
}
}Между делом – довольно удобная штука, позволяющая экономить на описании класса, но не в случае когда тип T – анонимный.
Вот такой вспомогательный метод
static IEqualityComparer<T> InferComparer<T>(
T _,
Func<T, T, bool> equals,
Func<T, int> gethashCode)
{
return CreateAdapter(equals, gethashCode);
}позволяет вывести тип компарера для анонимного типа.
var pairComparer = InferComparer( pair, (x, y) => x.Key.SequenceEqual(y.Key), x => x.Key.Aggregate(0, (h, s) => h ^ s.GetHashCode()));Эта конструкция создает IEqualityComparer для анонимного типа, свойство Key которого является коллекцией строк. Таким образом сравниваются экземпляры по совпадению коллекций строк, и хэшкод вычисляется по набору строк свойства Key анонимного типа. Далее этот компарер используется для построения словаря.