V minulém článku jsme zjistili, že pokud máme generickou metodu s obecným generickým parametrem T, nelze provést přetypování hodnotového typu na T bez nutnosti vložení boxing/unboxing operací. A jak jsem na konci minulého článku slíbil, nyní si ukážeme jedno možné řešení tohoto problému.
Řešení je založené na C# 3.0 expressions. Pro ty co se s touto technikou zatím nesetkali uvedu stručně o co se jedná. V technologii LINQ je reprezentován dotaz nad IQueryable zdrojem (dotaz využívající nějakého skutečného LINQ providera, nikoliv LINQ to Objects) pomoci tzv. expression tree. Jedná se o objektovou reprezentaci (objektový strom) libovolného syntakticky korektního C# výrazu. To co je pro nás nejdůležitější je to, že expression tree lze v době běhu aplikace převést na spustitelnou anonymní metodu.
Obvyklý postup použití expressions má tedy tyto tři kroky:
- V kódu sestrojíme objektovou reprezentaci výrazu (instance typu Expression<TDelegate>)
- Pomoci metody Compile jí převedeme na spustitelnou anonymní metodu
- Takto dynamicky sestrojenou metodu (obvykle opakovaně) zavoláme pomoci obdrženého delegátu
V našem řešení se konkrétně bude jednat o výraz pro přetypování nebo konverzi (cast operator), který se sestrojí metodou Expression.Convert. Výsledný výraz bude typu Func<TFrom, TTo>, kde generické parametry určující ze kterého a na jaký typ bude přetypování/konverze prováděná.
Druhou záležitostí, kterou zde s výhodou využijeme, je skutečnost, o které jsem již psal dříve, a sice že statické proměnné jsou per-T. (Vítejte zpět, můžeme pokračovat.)
Nyní již můžeme přistoupit k sestavení finálního řešení. Dle ukázaného postupu vytvoříme tedy generickou třídu, do které umístíme static field s_Converter obsahující sestrojený výraz “cacheovaný” pro kombinace generických argumentů TTo a TFrom. Z důvodu fungování type inference u argumentu TFrom, ještě třídu “rozdělíme” na třídu s argumentem TTo s vnořenou nested pomocnou třídou s argumentem TFrom.
Celý zdrojový kód výsledné třídy DynamicConverter je následující:
using System;
using System.Linq.Expressions;
namespace IMP.Shared
{
internal static class DynamicConverter<TTo>
{
#region member types definition
private static class ConverterFrom<TFrom>
{
#region member varible and default property initialization
internal static readonly Func<TFrom, TTo> s_Converter = CreateExpression<TFrom, TTo>(value => Expression.Convert(value, typeof(TTo)));
#endregion
#region private member functions
/// <summary>
/// Create a function delegate representing an operation
/// </summary>
/// <typeparam name="T">The parameter type</typeparam>
/// <typeparam name="TResult">The return type</typeparam>
/// <param name="body">Body factory</param>
/// <returns>Compiled function delegate</returns>
private static Func<T, TResult> CreateExpression<T, TResult>(Func<ParameterExpression, Expression> body)
{
var param = Expression.Parameter(typeof(T), "value");
try
{
return Expression.Lambda<Func<T, TResult>>(body(param), param).Compile();
}
catch (Exception ex)
{
string msg = ex.Message; //avoid capture of ex itself
return _ => { throw new InvalidOperationException(msg); };
}
}
#endregion
}
#endregion
#region action methods
/// <summary>
/// Performs a conversion between the given types; this will throw
/// an InvalidOperationException if the type T does not provide a suitable cast, or for
/// Nullable<TInner> if TInner does not provide this cast.
/// </summary>
public static TTo Convert<TFrom>(TFrom valueToConvert)
{
return ConverterFrom<TFrom>.s_Converter(valueToConvert);
}
#endregion
}
}
Ještě si všimněte, zavedené pomocné metody CreateExpression, do které lze “vsadit” konkrétní konstruovaný jednoparametrový výraz.
Třída je dostupná také ke stažení zde.
Použití třídy DynamicConverter bude vypadat takto:
class Foo
{
public static void Bar<T>() where T : struct
{
short s = 123;
int i = s;
T value1 = DynamicConverter<T>.Convert(i); //int=>T
T value2 = DynamicConverter<T>.Convert((int)s); //int=>T
T value3 = DynamicConverter<T>.Convert(s); //short=>T (sestavuje se jiný expression)
}
public static void Main()
{
Foo.Bar<int>();
}
}
Všechny konverze nám nyní fungují.
Stejný postup (sestrojení expression, static field v generické třídě) je samozřejmě možný obecně použít i pro jiné scénáře než pouze ukázaný dynamic converter.
Ještě jednou ale zopakuji nevýhodu použitého řešení. Přestože se konstrukce výrazu provádí pro každou kombinaci datových typů pouze jedenkrát, jedná se o časově náročnou operaci. (To platí pro kompilaci expression pomoci metody Compile obecně.) Je tedy třeba zvážit případně i změřit, zda je náročnost inicializace pro konkrétní scénář přijatelná.
Toto byl poslední příspěvek tento rok, přeji hodně štěstí v roce novém, tedy hlavně Happy coding.