web-dev-qa-db-fra.com

Est-il possible d'implémenter les mixins en C #?

J'ai entendu dire que c'était possible avec les méthodes d'extension, mais je ne peux pas tout comprendre moi-même. J'aimerais voir un exemple spécifique si possible.

Merci!

53
Stewart Johnson

Cela dépend vraiment de ce que vous entendez par "mixin" - tout le monde semble avoir une idée légèrement différente. Le type de mix que je aimerais voir (mais qui n’est pas disponible en C #) simplifie la mise en œuvre à travers la composition:

public class Mixin : ISomeInterface
{
    private SomeImplementation impl implements ISomeInterface;

    public void OneMethod()
    {
        // Specialise just this method
    }
}

Le compilateur implémenterait ISomeInterface simplement en envoyant un proxy à chaque membre pour qu'il "implicite" sauf s'il y avait une autre implémentation directement dans la classe.

Cependant, rien de tout cela n’est possible pour le moment :)

86
Jon Skeet

Il existe un framework open source qui vous permet d'implémenter des mixins via C #. Jetez un coup d'œil sur http://remix.codeplex.com/ .

Il est très facile d'implémenter des mixins avec ce framework. Il suffit de consulter les exemples et les liens "Informations complémentaires" figurant sur la page.

9
Stefan Papp

J'utilise habituellement ce modèle:

public interface IColor
{
    byte Red   {get;}
    byte Green {get;}
    byte Blue  {get;}
}

public static class ColorExtensions
{
    public static byte Luminance(this IColor c)
    {
        return (byte)(c.Red*0.3 + c.Green*0.59+ c.Blue*0.11);
    }
}

J'ai les deux définitions dans le même fichier source/espace de nom . Ainsi, les extensions sont toujours disponibles lorsque l'interface est utilisée (avec 'using').

Cela vous donne un mélange limité comme décrit dans le premier lien de CMS.

Limites:

  • pas de champs de données 
  • pas de propriétés (vous devrez appeler myColor.Luminance () avec des parenthèses, propriétés d'extension quelqu'un?) 

Cela reste suffisant pour de nombreuses situations.

Ce serait bien si (MS) pouvait ajouter de la magie au compilateur pour générer automatiquement la classe d’extension:

public interface IColor
{
    byte Red   {get;}
    byte Green {get;}
    byte Blue  {get;}

    // compiler generates anonymous extension class
    public static byte Luminance(this IColor c)     
    {
        return (byte)(c.Red*0.3 + c.Green*0.59+ c.Blue*0.11);
    }
}

Bien que l'astuce de compilation proposée par Jon serait encore plus intéressante.

7
3dGrabber

LinFu et DynamicProxy de Castle implémentent des mixins. COP (Composite Oriented Programming) pourrait être considéré comme un paradigme complet à partir de mixins. _ { Cet article de Anders Noras } _ contient des informations et des liens utiles.

EDIT: C’est possible avec C # 2.0, sans méthodes d’extension

4

Vous pouvez également augmenter l’approche de la méthode d’extension pour incorporer l’état, dans un modèle semblable aux propriétés attachées de WPF. 

Voici un exemple avec un passe-partout minimum. Notez qu'aucune modification n'est requise sur les classes cibles, y compris l'ajout d'interfaces, sauf si vous devez traiter la classe cible de manière polymorphe - auquel cas vous vous retrouvez avec quelque chose de très proche de l'héritage multiple réel.

// Mixin class: mixin infrastructure and mixin component definitions
public static class Mixin
{ 
    // =====================================
    // ComponentFoo: Sample mixin component
    // =====================================

    //  ComponentFooState: ComponentFoo contents
    class ComponentFooState
    {
        public ComponentFooState() {
            // initialize as you like
            this.Name = "default name";
        }

        public string Name { get; set; }
    }

    // ComponentFoo methods

    // if you like, replace T with some interface 
    // implemented by your target class(es)

    public static void 
    SetName<T>(this T obj, string name) {
        var state = GetState(component_foo_states, obj);

        // do something with "obj" and "state"
        // for example: 

        state.Name = name + " the " + obj.GetType();


    }
    public static string
    GetName<T>(this T obj) {
        var state = GetState(component_foo_states, obj);

        return state.Name; 
    }

    // =====================================
    // boilerplate
    // =====================================

    //  instances of ComponentFoo's state container class,
    //  indexed by target object
    static readonly Dictionary<object, ComponentFooState>
    component_foo_states = new Dictionary<object, ComponentFooState>();

    // get a target class object's associated state
    // note lazy instantiation
    static TState
    GetState<TState>(Dictionary<object, TState> dict, object obj) 
    where TState : new() {
        TState ret;
        if(!dict.TryGet(obj, out ret))
            dict[obj] = ret = new TState();

        return ret;
    }

}

Usage:

var some_obj = new SomeClass();
some_obj.SetName("Johny");
Console.WriteLine(some_obj.GetName()); // "Johny the SomeClass"

Notez que cela fonctionne aussi avec des instances nulles, comme le font naturellement les méthodes d’extension.

Vous pouvez également envisager d'utiliser une implémentation WeakDictionary pour éviter les fuites de mémoire causées par le fait que la collection conserve des références de classe cibles en tant que clés.

3
staafl

J'avais besoin de quelque chose de similaire, alors j'ai conçu ce qui suit avec Reflection.Emit. Dans le code suivant, un nouveau type est généré dynamiquement avec un membre privé de type 'mixin'. Tous les appels aux méthodes d'interface 'mixin' sont transférés à ce membre privé. Un constructeur de paramètre unique est défini. Il prend une instance qui implémente l'interface 'mixin'. Fondamentalement, cela équivaut à écrire le code suivant pour un type concret T et une interface I donnés:

class Z : T, I
{
    I impl;

    public Z(I impl)
    {
        this.impl = impl;
    }

    // Implement all methods of I by proxying them through this.impl
    // as follows: 
    //
    // I.Foo()
    // {
    //    return this.impl.Foo();
    // }
}

C'est la classe:

public class MixinGenerator
{
    public static Type CreateMixin(Type @base, Type mixin)
    {
        // Mixin must be an interface
        if (!mixin.IsInterface)
            throw new ArgumentException("mixin not an interface");

        TypeBuilder typeBuilder = DefineType(@base, new Type[]{mixin});

        FieldBuilder fb = typeBuilder.DefineField("impl", mixin, FieldAttributes.Private);

        DefineConstructor(typeBuilder, fb);

        DefineInterfaceMethods(typeBuilder, mixin, fb);

        Type t = typeBuilder.CreateType();

        return t;
    }

    static AssemblyBuilder assemblyBuilder;
    private static TypeBuilder DefineType(Type @base, Type [] interfaces)
    {
        assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
            new AssemblyName(Guid.NewGuid().ToString()), AssemblyBuilderAccess.RunAndSave);

        ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(Guid.NewGuid().ToString());

        TypeBuilder b = moduleBuilder.DefineType(Guid.NewGuid().ToString(),
            @base.Attributes,
            @base,
            interfaces);

        return b;
    }
    private static void DefineConstructor(TypeBuilder typeBuilder, FieldBuilder fieldBuilder)
    {
        ConstructorBuilder ctor = typeBuilder.DefineConstructor(
            MethodAttributes.Public, CallingConventions.Standard, new Type[] { fieldBuilder.FieldType });

        ILGenerator il = ctor.GetILGenerator();

        // Call base constructor
        ConstructorInfo baseCtorInfo =  typeBuilder.BaseType.GetConstructor(new Type[]{});
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Call, typeBuilder.BaseType.GetConstructor(new Type[0]));

        // Store type parameter in private field
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Stfld, fieldBuilder);
        il.Emit(OpCodes.Ret);
    }

    private static void DefineInterfaceMethods(TypeBuilder typeBuilder, Type mixin, FieldInfo instanceField)
    {
        MethodInfo[] methods = mixin.GetMethods();

        foreach (MethodInfo method in methods)
        {
            MethodInfo fwdMethod = instanceField.FieldType.GetMethod(method.Name,
                method.GetParameters().Select((pi) => { return pi.ParameterType; }).ToArray<Type>());

            MethodBuilder methodBuilder = typeBuilder.DefineMethod(
                                            fwdMethod.Name,
                                            // Could not call absract method, so remove flag
                                            fwdMethod.Attributes & (~MethodAttributes.Abstract),
                                            fwdMethod.ReturnType,
                                            fwdMethod.GetParameters().Select(p => p.ParameterType).ToArray());

            methodBuilder.SetReturnType(method.ReturnType);
            typeBuilder.DefineMethodOverride(methodBuilder, method);

            // Emit method body
            ILGenerator il = methodBuilder.GetILGenerator();
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldfld, instanceField);

            // Call with same parameters
            for (int i = 0; i < method.GetParameters().Length; i++)
            {
                il.Emit(OpCodes.Ldarg, i + 1);
            }
            il.Emit(OpCodes.Call, fwdMethod);
            il.Emit(OpCodes.Ret);
        }
    }
}

C'est l'usage:

public interface ISum
{
    int Sum(int x, int y);
}

public class SumImpl : ISum
{
    public int Sum(int x, int y)
    {
        return x + y;
    }
}

public class Multiply
{        
    public int Mul(int x, int y)
    {
        return x * y;
    }
}

// Generate a type that does multiply and sum
Type newType = MixinGenerator.CreateMixin(typeof(Multiply), typeof(ISum));

object instance = Activator.CreateInstance(newType, new object[] { new SumImpl() });

int res = ((Multiply)instance).Mul(2, 4);
Console.WriteLine(res);
res = ((ISum)instance).Sum(1, 4);
Console.WriteLine(res);
2
mll5

Si vous avez une classe de base capable de stocker des données, vous pouvez appliquer la sécurité du compilateur et utiliser des interfaces de marqueur .. C'est en gros ce que "Mixins in C # 3.0" propose dans la réponse acceptée. 

public static class ModelBaseMixins
{
    public interface IHasStuff{ }

    public static void AddStuff<TObjectBase>(this TObjectBase objectBase, Stuff stuff) where TObjectBase: ObjectBase, IHasStuff
    {
        var stuffStore = objectBase.Get<IList<Stuff>>("stuffStore");
        stuffStore.Add(stuff);
    }
}

La ObjectBase:

public abstract class ObjectBase
{
    protected ModelBase()
    {
        _objects = new Dictionary<string, object>();
    }

    private readonly Dictionary<string, object> _objects;

    internal void Add<T>(T thing, string name)
    {
        _objects[name] = thing;
    }

    internal T Get<T>(string name)
    {
        T thing = null;
        _objects.TryGetValue(name, out thing);

        return (T) thing;
    }

Donc, si vous avez une classe que vous pouvez hériter de 'ObjectBase' et décorer avec IHasStuff, vous pouvez maintenant ajouter sutff

1
regisbsb

J'ai trouvé une solution de contournement ici , qui, sans être tout à fait élégante, vous permet d'obtenir un comportement de mixin parfaitement observable. De plus, IntelliSense fonctionne toujours!

using System;
using System.Runtime.CompilerServices; //needed for ConditionalWeakTable
public interface MAgeProvider // use 'M' prefix to indicate mixin interface
{
    // nothing needed in here, it's just a 'marker' interface
}
public static class AgeProvider // implements the mixin using extensions methods
{
    static ConditionalWeakTable<MAgeProvider, Fields> table;
    static AgeProvider()
    {
        table = new ConditionalWeakTable<MAgeProvider, Fields>();
    }
    private sealed class Fields // mixin's fields held in private nested class
    {
        internal DateTime BirthDate = DateTime.UtcNow;
    }
    public static int GetAge(this MAgeProvider map)
    {
        DateTime dtNow = DateTime.UtcNow;
        DateTime dtBorn = table.GetOrCreateValue(map).BirthDate;
        int age = ((dtNow.Year - dtBorn.Year) * 372
                   + (dtNow.Month - dtBorn.Month) * 31
                   + (dtNow.Day - dtBorn.Day)) / 372;
        return age;
    }
    public static void SetBirthDate(this MAgeProvider map, DateTime birthDate)
    {
        table.GetOrCreateValue(map).BirthDate = birthDate;
    }
}

public abstract class Animal
{
    // contents unimportant
}
public class Human : Animal, MAgeProvider
{
    public string Name;
    public Human(string name)
    {
        Name = name;
    }
    // nothing needed in here to implement MAgeProvider
}
static class Test
{
    static void Main()
    {
        Human h = new Human("Jim");
        h.SetBirthDate(new DateTime(1980, 1, 1));
        Console.WriteLine("Name {0}, Age = {1}", h.Name, h.GetAge());
        Human h2 = new Human("Fred");
        h2.SetBirthDate(new DateTime(1960, 6, 1));
        Console.WriteLine("Name {0}, Age = {1}", h2.Name, h2.GetAge());
        Console.ReadKey();
    }
}
0
BartoszKP

Voici une implémentation de mixin que je viens de proposer. Je vais probablement l'utiliser avec une bibliothèque de moi .

Cela a probablement déjà été fait quelque part.

Tout est typé statiquement, sans dictionnaires ni quelque chose. Cela nécessite un peu de code supplémentaire par type, vous n'avez pas besoin de stockage par instance. D'autre part, cela vous donne également la possibilité de modifier l'implémentation de mixin à la volée, si vous le souhaitez. Aucun outil post-build, pre-build, mid-build.

Il a certaines limites, mais il permet des choses comme le dépassement, etc.

Nous commençons par définir une interface de marqueur. Peut-être que quelque chose sera ajouté plus tard:

public interface Mixin {}

Cette interface est implémentée par mixins. Les mixins sont des cours réguliers. Les types n'héritent pas ou n'implémentent pas directement les mixins. Au lieu de cela, ils exposent simplement une instance du mixin à l'aide de l'interface:

public interface HasMixins {}

public interface Has<TMixin> : HasMixins
    where TMixin : Mixin {
    TMixin Mixin { get; }
}

Implémenter cette interface signifie supporter le mixin. Il est important que cela soit implémenté explicitement, car nous allons en avoir plusieurs par type.

Maintenant, pour un petit truc en utilisant des méthodes d'extension. Nous définissons:

public static class MixinUtils {
    public static TMixin Mixout<TMixin>(this Has<TMixin> what)
        where TMixin : Mixin {
        return what.Mixin;
    }
}

Mixout expose le mixin du type approprié. Maintenant, pour tester cela, définissons:

public abstract class Mixin1 : Mixin {}

public abstract class Mixin2 : Mixin {}

public abstract class Mixin3 : Mixin {}

public class Test : Has<Mixin1>, Has<Mixin2> {

    private class Mixin1Impl : Mixin1 {
        public static readonly Mixin1Impl Instance = new Mixin1Impl();
    }

    private class Mixin2Impl : Mixin2 {
        public static readonly Mixin2Impl Instance = new Mixin2Impl();
    }

    Mixin1 Has<Mixin1>.Mixin => Mixin1Impl.Instance;

    Mixin2 Has<Mixin2>.Mixin => Mixin2Impl.Instance;
}

static class TestThis {
    public static void run() {
        var t = new Test();
        var a = t.Mixout<Mixin1>();
        var b = t.Mixout<Mixin2>();
    }
}

De manière plutôt amusante (bien que rétrospectivement, cela ait un sens), IntelliSense ne détecte pas que la méthode d'extension Mixout s'applique à Test, mais le compilateur l'accepte, tant que Test dispose du mixin. Si tu essayes,

t.Mixout<Mixin3>();

Cela vous donne une erreur de compilation.

Vous pouvez aller un peu de fantaisie, et définir la méthode suivante aussi:

[Obsolete("The object does not have this mixin.", true)]
public static TSome Mixout<TSome>(this HasMixins something) where TSome : Mixin {
    return default(TSome);
}

Cela permet: a) d’afficher une méthode appelée Mixout dans IntelliSense, rappelant son existence, et b) de fournir un message d’erreur un peu plus descriptif (généré par l’attribut Obsolete).

0
GregRos