Jak napisać generyczną klasę kontenera, która implementuje dany interfejs w C#?
Kontekst:. NET 3.5, VS2008. Nie jestem pewien co do tytułu tego pytania, więc zapraszam do komentowania tytułu również: -)
Oto scenariusz: mam kilka klas, powiedzmy Foo i Bar, wszystkie implementują następujący interfejs:
public interface IStartable
{
void Start();
void Stop();
}
A teraz chciałbym mieć klasę kontenera, która w swoim konstruktorze otrzymuje jako argument wartość IEnumerable
public class StartableGroup : IStartable // this is the container class
{
private readonly IEnumerable<IStartable> startables;
public StartableGroup(IEnumerable<IStartable> startables)
{
this.startables = startables;
}
public void Start()
{
foreach (var startable in startables)
{
startable.Start();
}
}
public void Stop()
{
foreach (var startable in startables)
{
startable.Stop();
}
}
}
Więc moje pytanie brzmi:: Jak mogę to zrobić bez ręcznego pisania kodu i bez generowania kodu? Innymi słowy, chciałbym mieć coś takiego jak poniżej.
var arr = new IStartable[] { new Foo(), new Bar("wow") };
var mygroup = GroupGenerator<IStartable>.Create(arr);
mygroup.Start(); // --> calls Foo's Start and Bar's Start
Ograniczenia:
- brak generowania kodu (tzn. brak rzeczywistego kodu tekstowego w czasie kompilacji)
- interfejs ma tylko metody void, z argumentami lub bez
Motywacja:
- mam dość dużą aplikację, z dużą ilością wtyczek różnych interfejsów. Ręcznie pisanie klasy "group container" dla każdego interfejsu "przeciąża" projekt klasami
- Ręczne pisanie kodu jest podatne na błędy
- wszelkie dodatki lub aktualizacje podpisu interfejsu IStartable doprowadzą do (ręcznych) zmian w klasie "group container"
- Nauka
Rozumiem, że muszę użyć reflection tutaj, ale wolałbym użyć solidnego frameworka (jak Castle ' s DynamicProxy lub RunSharp ) do zrobienia okablowania dla ja.
Jakieś pomysły?7 answers
To nie jest ładne, ale wydaje się działać:
public static class GroupGenerator
{
public static T Create<T>(IEnumerable<T> items) where T : class
{
return (T)Activator.CreateInstance(Cache<T>.Type, items);
}
private static class Cache<T> where T : class
{
internal static readonly Type Type;
static Cache()
{
if (!typeof(T).IsInterface)
{
throw new InvalidOperationException(typeof(T).Name
+ " is not an interface");
}
AssemblyName an = new AssemblyName("tmp_" + typeof(T).Name);
var asm = AppDomain.CurrentDomain.DefineDynamicAssembly(
an, AssemblyBuilderAccess.RunAndSave);
string moduleName = Path.ChangeExtension(an.Name,"dll");
var module = asm.DefineDynamicModule(moduleName, false);
string ns = typeof(T).Namespace;
if (!string.IsNullOrEmpty(ns)) ns += ".";
var type = module.DefineType(ns + "grp_" + typeof(T).Name,
TypeAttributes.Class | TypeAttributes.AnsiClass |
TypeAttributes.Sealed | TypeAttributes.NotPublic);
type.AddInterfaceImplementation(typeof(T));
var fld = type.DefineField("items", typeof(IEnumerable<T>),
FieldAttributes.Private);
var ctor = type.DefineConstructor(MethodAttributes.Public,
CallingConventions.HasThis, new Type[] { fld.FieldType });
var il = ctor.GetILGenerator();
// store the items
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Stfld, fld);
il.Emit(OpCodes.Ret);
foreach (var method in typeof(T).GetMethods())
{
var args = method.GetParameters();
var methodImpl = type.DefineMethod(method.Name,
MethodAttributes.Private | MethodAttributes.Virtual,
method.ReturnType,
Array.ConvertAll(args, arg => arg.ParameterType));
type.DefineMethodOverride(methodImpl, method);
il = methodImpl.GetILGenerator();
if (method.ReturnType != typeof(void))
{
il.Emit(OpCodes.Ldstr,
"Methods with return values are not supported");
il.Emit(OpCodes.Newobj, typeof(NotSupportedException)
.GetConstructor(new Type[] {typeof(string)}));
il.Emit(OpCodes.Throw);
continue;
}
// get the iterator
var iter = il.DeclareLocal(typeof(IEnumerator<T>));
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldfld, fld);
il.EmitCall(OpCodes.Callvirt, typeof(IEnumerable<T>)
.GetMethod("GetEnumerator"), null);
il.Emit(OpCodes.Stloc, iter);
Label tryFinally = il.BeginExceptionBlock();
// jump to "progress the iterator"
Label loop = il.DefineLabel();
il.Emit(OpCodes.Br_S, loop);
// process each item (invoke the paired method)
Label doItem = il.DefineLabel();
il.MarkLabel(doItem);
il.Emit(OpCodes.Ldloc, iter);
il.EmitCall(OpCodes.Callvirt, typeof(IEnumerator<T>)
.GetProperty("Current").GetGetMethod(), null);
for (int i = 0; i < args.Length; i++)
{ // load the arguments
switch (i)
{
case 0: il.Emit(OpCodes.Ldarg_1); break;
case 1: il.Emit(OpCodes.Ldarg_2); break;
case 2: il.Emit(OpCodes.Ldarg_3); break;
default:
il.Emit(i < 255 ? OpCodes.Ldarg_S
: OpCodes.Ldarg, i + 1);
break;
}
}
il.EmitCall(OpCodes.Callvirt, method, null);
// progress the iterator
il.MarkLabel(loop);
il.Emit(OpCodes.Ldloc, iter);
il.EmitCall(OpCodes.Callvirt, typeof(IEnumerator)
.GetMethod("MoveNext"), null);
il.Emit(OpCodes.Brtrue_S, doItem);
il.Emit(OpCodes.Leave_S, tryFinally);
// dispose iterator
il.BeginFinallyBlock();
Label endFinally = il.DefineLabel();
il.Emit(OpCodes.Ldloc, iter);
il.Emit(OpCodes.Brfalse_S, endFinally);
il.Emit(OpCodes.Ldloc, iter);
il.EmitCall(OpCodes.Callvirt, typeof(IDisposable)
.GetMethod("Dispose"), null);
il.MarkLabel(endFinally);
il.EndExceptionBlock();
il.Emit(OpCodes.Ret);
}
Cache<T>.Type = type.CreateType();
#if DEBUG // for inspection purposes...
asm.Save(moduleName);
#endif
}
}
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-05-12 06:53:32
To nie jest tak czysty interfejs jak rozwiązanie oparte na odbiciu, ale bardzo prostym i elastycznym rozwiązaniem jest stworzenie metody ForAll w ten sposób:
static void ForAll<T>(this IEnumerable<T> items, Action<T> action)
{
foreach (T item in items)
{
action(item);
}
}
I można nazwać tak:
arr.ForAll(x => x.Start());
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-05-11 16:19:20
Możesz podklasować List<T>
lub inną klasę kolekcji i użyć ograniczenia typu ogólnego where
, aby ograniczyć typ T
do klasy IStartable
.
class StartableList<T> : List<T>, IStartable where T : IStartable
{
public StartableList(IEnumerable<T> arr)
: base(arr)
{
}
public void Start()
{
foreach (IStartable s in this)
{
s.Start();
}
}
public void Stop()
{
foreach (IStartable s in this)
{
s.Stop();
}
}
}
Możesz również zadeklarować klasę w ten sposób, jeśli nie chcesz, aby była to klasa generyczna wymagająca parametru type.
public class StartableList : List<IStartable>, IStartable
{ ... }
Twój przykładowy kod użycia będzie wyglądał mniej więcej tak:
var arr = new IStartable[] { new Foo(), new Bar("wow") };
var mygroup = new StartableList<IStartable>(arr);
mygroup.Start(); // --> calls Foo's Start and Bar's Start
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-05-11 12:51:46
Automapper
jest dobrym rozwiązaniem. Polega na LinFu
poniżej, aby utworzyć instancję, która implementuje interfejs, ale dba o niektóre z uwodnienia i mixins pod nieco płynne api. Autor LinFu
twierdzi, że jest on w rzeczywistości znacznie lżejszy i szybszy niż Castle
' S Proxy
.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-19 16:52:19
Możesz poczekać na C # 4.0 i użyć dynamicznego wiązania.
To świetny pomysł - musiałem go wdrożyć do IDisposable kilka razy; kiedy chcę wiele rzeczy do usunięcia. Należy jednak pamiętać o tym, jak błędy będą obsługiwane. Powinno się logować i uruchamiać inne itp... Potrzebowałbyś kilku opcji, żeby dać lekcję.
Nie jestem zaznajomiony z DynamicProxy i jak można go tutaj użyć.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-05-11 12:19:41
Możesz użyć klasy "List" i ich metody "ForEach".
var startables = new List<IStartable>( array_of_startables );
startables.ForEach( t => t.Start(); }
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-05-11 12:44:21
Jeśli dobrze rozumiem, prosisz o implementację "GroupGenerator".
Bez żadnego prawdziwego doświadczenia z CastleProxy moim zaleceniem byłoby użycie GetMethods (), aby uzyskać początkowe metody wymienione w interfejsie, a następnie utworzyć nowy typ w locie za pomocą Reflection.Emit z nowymi metodami, które wyliczają przez obiekty i wywołują każdą odpowiadającą im metodę. Wydajność nie powinna być taka zła.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-05-11 12:52:07