2009-05-11 13 views
24

Kontekst: .NET 3.5, VS2008. Nie jestem pewien co do tytułu tego pytania, więc nie krępuj się komentować tytuł :-)Jak napisać ogólną klasę kontenera, która implementuje dany interfejs w języku C#?

Oto scenariusz: Mam kilka klas, na przykład Foo i Bar, wszystkie implementują następujący interfejs :

public interface IStartable 
{ 
    void Start(); 
    void Stop(); 
} 

a teraz chciałbym mieć klasę pojemnik, który dostaje IEnumerable <IStartable> jako argument konstruktora. Klasa ta, z kolei, należy również wdrożyć IStartable interfejs:

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: w jaki sposób 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:

  • Nie generowania kodu (czyli nie prawdziwy kod tekstowy w czasie kompilacji)
  • interfejs ma tylko void metod, z lub bez argumentów

Motywacja:

  • Mam dość dużą aplikację, z wieloma wtyczkami różnych interfejsów. Ręczne pisanie klasy "kontener grupowy" dla każdego interfejsu "przeciąża" projekt z klasami
  • Ręczne pisanie kodu jest podatne na błędy
  • Wszelkie dodatki lub aktualizacje sygnatur do interfejsu IStartable spowodują (ręczne) zmiany w " pojemnik grupa”klasa
  • Nauka

rozumiem, że muszę użyć refleksji tutaj, ale wolałbym używać solidnych ram (jak Castle'a DynamicProxy lub RunSharp) zrobić okablowanie dla mnie.

Jakieś myśli?

+0

Czyli * nie chcesz mieć klasy StartableGroup? Co jest z tym nie tak? – Noldorin

+0

Czy mogę zapytać: dlaczego? Jaki problem musi rozwiązać ten problem? (może to wpłynąć na odpowiedź ...). –

+0

@Noldorin, @Marc Gravell, dodano motywację do pierwotnego pytania. –

Odpowiedz

26

To nie jest ładna, 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 
     } 
    } 
} 
+0

Zapisałeś mi trochę pisania :-) –

+0

Myślę, że masz niewielki błąd (nie skompilujesz): Zamiast: Pamięć podręczna .Type = type.CreateType(); Powinno być: Typ = typ.CreateType(); –

+0

Próbowałem sugerowanego kodu i wygląda na to, że twoja odpowiedź nie obejmuje metod z argumentami (zobacz ograniczenie "Interfejs ma tylko puste metody, z lub bez argumentów"). Obecnie istnieje wyjątek, gdy interfejs zawiera metodę z pojedynczym argumentem. –

2

Można podklasy List<T> lub innej klasy zbierania i korzystania z where rodzajowe typu ograniczenie ograniczyć rodzaj T być tylko IStartable klas.

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żna również zadeklarować klasę, jeśli nie chcesz, aby była klasą ogólną wymagającą parametru typu.

public class StartableList : List<IStartable>, IStartable 
{ ... } 

Kod Wykorzystanie próbka będzie wtedy wyglądać 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 
+1

Myślę, że to nie odpowiada na pytanie. – DonkeyMaster

+0

@DonkeyMaster - Nie, nie odpowiada na dokładne pytanie, ale myślę, że jest to możliwe, jeśli poprawnie podniosę pytanie. Mój post oferuje ręcznie napisane rozwiązanie, doskonała próbka Marc Gravell oferuje rozwiązanie do generowania kodu (runtime). Nie wiem, jak to zrobić bez tego: bez tego oryginalny plakat prosił o rozwiązanie "bez ręcznego pisania kodu, bez generowania kodu". –

+0

Rzeczywiście, jak zauważył @DonkeyMaster, to nie odpowiada na pytanie. Dzięki temu kod jest bardziej przejrzysty i może bardziej elegancki, ale pozostaje pytanie: w jaki sposób mogę utworzyć taki kod w czasie wykonywania, bez konieczności pisania (lub generowania go) w czasie projektowania? –

0

Można czekać na C# 4.0 i wykorzystać dynamiczny wiążące.

To świetny pomysł - musiałem wdrożyć to dla IDisposable przy kilku okazjach; kiedy chcę, aby wiele rzeczy zostało usuniętych.Należy jednak pamiętać o sposobie postępowania z błędami. Czy powinien logować się i uruchamiać inne, itd ... Potrzebne są pewne opcje, aby dać klasę.

Nie jestem zaznajomiony z DynamicProxy i jak można go tutaj wykorzystać.

+0

C# 4.0 nie będzie tu przez jakiś czas. Nie ma jeszcze jeszcze CTP! – DonkeyMaster

0

Możesz użyć klasy "List" i ich metody "ForEach".

var startables = new List<IStartable>(array_of_startables); 
startables.ForEach(t => t.Start(); } 
+0

To jest pierwsza rzecz, która przyszła mi do głowy - ale on prosi o implementację powyższej klasy "GroupGenerator". –

0

Jeśli dobrze rozumiem, prosimy o wdrożenie "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 wyliczonymi przez obiekty i wywołaj każdą odpowiednią metodę. Występ nie powinien być zbyt zły.

4

To nie jest tak czysty interfejs jako rozwiązanie refleksji oparty, ale bardzo proste i elastyczne rozwiązanie jest stworzenie metody forall jak tak :

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()); 
2

Automapper to dobre rozwiązanie. Opiera się on na LinFu poniżej, aby utworzyć instancję, która implementuje interfejs, ale zajmuje się niektórymi procesami nawadniania i miksami w nieco płynnym interfejsie API. Autor LinFu twierdzi, że jest w rzeczywistości znacznie lżejszy i szybszy niż 's .

+0

Dzięki za cynk, zagłębię się w to, kiedy będę miał czas. –

Powiązane problemy