Запрос функции С#: реализовать интерфейсы на анонимных типах

Мне интересно, что нужно сделать, чтобы сделать что-то вроде этой работы:

using System;

class Program
{
    static void Main()
    {
        var f = new IFoo { 
                    Foo = "foo",
                    Print = () => Console.WriteLine(Foo)
            };
    }
}

interface IFoo
{
    String Foo { get; set; }
    void Print();
}

Созданный анонимный тип будет выглядеть примерно так:

internal sealed class <>f__AnonymousType0<<Foo>j__TPar> : IFoo
{
    readonly <Foo>j__TPar <Foo>i__Field;

    public <>f__AnonymousType0(<Foo>j__TPar Foo)
    {
        this.<Foo>i__Field = Foo;
    }

    public <Foo>j__TPar Foo
    {
        get { return this.<Foo>i__Field; }
    }

    public void Print()
    {
        Console.WriteLine(this.Foo);
    }
}

Есть ли причина, по которой компилятор не сможет сделать что-то подобное? Даже для непустых методов или методов, которые принимают параметры, компилятор должен иметь возможность выводить типы из декларации интерфейса.

Отказ от ответственности: Пока я понимаю, что в настоящее время это невозможно, и было бы разумнее просто создать конкретный класс в этом случае. Меня больше интересуют теоретические аспекты этого.

Ответы

Ответ 1

Было бы несколько проблем с перегруженными членами, индексаторами и явными реализациями интерфейса.

Однако вы могли бы определить синтаксис таким образом, чтобы разрешить эти проблемы.

Интересно, что вы можете приблизиться к тому, что хотите с С# 3.0, написав библиотеку. В принципе, вы можете сделать это:

Create<IFoo>
(
    new
    {
        Foo = "foo",
        Print = (Action)(() => Console.WriteLine(Foo))
    }
);

Это довольно близко к тому, что вы хотите. Основными отличиями являются призыв "Создать" вместо "нового" ключевого слова и тот факт, что вам нужно указать тип делегата.

Объявление "Создать" будет выглядеть так:

T Create<T> (object o)
{
//...
}

Затем он будет использовать Reflection.Emit для динамического создания реализации интерфейса во время выполнения.

Этот синтаксис, однако, имеет проблемы с явными реализациями интерфейса и перегруженными членами, которые невозможно решить без изменения компилятора.

Альтернативой может быть использование инициализатора коллекции, а не анонимного типа. Это будет выглядеть так:

Create
{
    new Members<IFoo>
    {
        {"Print", ((IFoo @this)=>Console.WriteLine(Foo))},
        {"Foo", "foo"}
    }
}

Это позволит вам:

  • Обработать явную реализацию интерфейса, указав что-то вроде "IEnumerable.Current" для строкового параметра.
  • Определить Members.Add, чтобы вам не нужно указывать тип делегата в инициализаторе.

Вам нужно сделать несколько вещей, чтобы реализовать это:

  • Writer - небольшой парсер для имен типов С#. Для этого требуется только ".", "[]", "< > ", ID и имена примитивных типов, поэтому вы могли бы сделать это через несколько часов.
  • Реализовать кеш, чтобы вы генерировали только один класс для каждого уникального интерфейса
  • Внедрить код Reflection.Emit. Это, вероятно, займет не более 2 дней.

Ответ 2

Для этого требуется С# 4, но интерфейс импровизированного интерфейса может подделать его из коробки, используя внутренние прокси DLR. Производительность хороша, хотя и не так хороша, как если бы предложенное вами изменение существовало.

using ImpromptuInterface.Dynamic;

...

var f = ImpromptuGet.Create<IFoo>(new{ 
                Foo = "foo",
                Print = ReturnVoid.Arguments(() => Console.WriteLine(Foo))
            });

Ответ 3

Анонимный тип не может быть сделан, чтобы делать что-либо, кроме как иметь свойства только для чтения.

Цитирование Руководство по программированию на С# (анонимные типы):

"Анонимные типы - это типы классов, которые состоят из одного или нескольких публичных только для чтения. Никаких других видов членов класса, таких как методы или события разрешены. Анонимный тип не могут быть добавлены к любому интерфейсу или тип, за исключением объекта."

Ответ 4

Пока мы собираем список желаний интерфейса, мне бы очень хотелось сообщить компилятору, что класс реализует интерфейс вне определения класса - даже в отдельной сборке.

Например, скажем, я работаю над программой для извлечения файлов из разных архивных форматов. Я хочу, чтобы иметь возможность использовать существующие реализации из разных библиотек — скажем, SharpZipLib и коммерческая реализация PGP — и потребляют обе библиотеки, используя один и тот же код, без создания новых классов. Тогда я мог бы использовать типы из любого источника в общих ограничениях, например.

Другим использованием было бы сказать компилятору, что System.Xml.Serialization.XmlSerializer реализует интерфейс System.Runtime.Serialization.IFormatter (он уже делает, но компилятор этого не знает).

Это может быть использовано для реализации вашего запроса, а не автоматически. Вам все равно придется явно сообщать компилятору об этом. Не знаете, как будет выглядеть синтаксис, потому что вам все равно придется вручную сопоставлять методы и свойства, что означает много словесности. Возможно, что-то похожее на методы расширения.

Ответ 5

У вас может быть что-то вроде анонимных классов в Java:

using System; 

class Program { 
  static void Main() { 
    var f = new IFoo() {  
      public String Foo { get { return "foo"; } } 
      public void Print() { Console.WriteLine(Foo); }
    }; 
  } 
} 

interface IFoo { 
  String Foo { get; set; } 
  void Print(); 
} 

Ответ 6

Разве это не было бы круто. Inline анонимный класс:

List<Student>.Distinct(new IEqualityComparer<Student>() 
{ 
    public override bool Equals(Student x, Student y)
    {
        return x.Id == y.Id;
    }

    public override int GetHashCode(Student obj)
    {
        return obj.Id.GetHashCode();
    }
})

Ответ 7

Я собираюсь свалить это здесь. Я написал это некоторое время назад, но IIRC работает нормально.

Сначала вспомогательная функция принимает MethodInfo и возвращает Type для сопоставления Func или Action. К сожалению, вам нужна ветка для каждого количества параметров, и я, по-видимому, остановился в три раза.

static Type GenerateFuncOrAction(MethodInfo method)
{
    var typeParams = method.GetParameters().Select(p => p.ParameterType).ToArray();
    if (method.ReturnType == typeof(void))
    {
        if (typeParams.Length == 0)
        {
            return typeof(Action);
        }
        else if (typeParams.Length == 1)
        {
            return typeof(Action<>).MakeGenericType(typeParams);
        }
        else if (typeParams.Length == 2)
        {
            return typeof(Action<,>).MakeGenericType(typeParams);
        }
        else if (typeParams.Length == 3)
        {
            return typeof(Action<,,>).MakeGenericType(typeParams);
        }
        throw new ArgumentException("Only written up to 3 type parameters");
    }
    else
    {
        if (typeParams.Length == 0)
        {
            return typeof(Func<>).MakeGenericType(typeParams.Concat(new[] { method.ReturnType }).ToArray());
        }
        else if (typeParams.Length == 1)
        {
            return typeof(Func<,>).MakeGenericType(typeParams.Concat(new[] { method.ReturnType }).ToArray());
        }
        else if (typeParams.Length == 2)
        {
            return typeof(Func<,,>).MakeGenericType(typeParams.Concat(new[] { method.ReturnType }).ToArray());
        }
        else if (typeParams.Length == 3)
        {
            return typeof(Func<,,,>).MakeGenericType(typeParams.Concat(new[] { method.ReturnType }).ToArray());
        }
        throw new ArgumentException("Only written up to 3 type parameters");
    }
}

И теперь метод, который принимает интерфейс как общий параметр и возвращает Type, который реализует интерфейс, и имеет конструктор (должен быть вызван через Activator.CreateInstance), беря Func или Action для каждого метод/геттер/сеттер. Однако вам нужно знать правильный порядок, чтобы поместить их в конструктор. Альтернативно (код с комментариями) он может генерировать DLL, которую вы можете ссылаться и использовать тип напрямую.

static Type GenerateInterfaceImplementation<TInterface>()
{
    var interfaceType = typeof(TInterface);
    var funcTypes = interfaceType.GetMethods().Select(GenerateFuncOrAction).ToArray();

    AssemblyName aName =
        new AssemblyName("Dynamic" + interfaceType.Name + "WrapperAssembly");
    var assBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
            aName,
            AssemblyBuilderAccess.Run/*AndSave*/); // to get a DLL

    var modBuilder = assBuilder.DefineDynamicModule(aName.Name/*, aName.Name + ".dll"*/); // to get a DLL

    TypeBuilder typeBuilder = modBuilder.DefineType(
        "Dynamic" + interfaceType.Name + "Wrapper",
            TypeAttributes.Public);

    // Define a constructor taking the same parameters as this method.
    var ctrBuilder = typeBuilder.DefineConstructor(
        MethodAttributes.Public | MethodAttributes.HideBySig |
            MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
        CallingConventions.Standard,
        funcTypes);


    // Start building the constructor.
    var ctrGenerator = ctrBuilder.GetILGenerator();
    ctrGenerator.Emit(OpCodes.Ldarg_0);
    ctrGenerator.Emit(
        OpCodes.Call,
        typeof(object).GetConstructor(Type.EmptyTypes));

    // For each interface method, we add a field to hold the supplied
    // delegate, code to store it in the constructor, and an
    // implementation that calls the delegate.
    byte methodIndex = 0;
    foreach (var interfaceMethod in interfaceType.GetMethods())
    {
        ctrBuilder.DefineParameter(
            methodIndex + 1,
            ParameterAttributes.None,
            "del_" + interfaceMethod.Name);

        var delegateField = typeBuilder.DefineField(
            "del_" + interfaceMethod.Name,
            funcTypes[methodIndex],
            FieldAttributes.Private);

        ctrGenerator.Emit(OpCodes.Ldarg_0);
        ctrGenerator.Emit(OpCodes.Ldarg_S, methodIndex + 1);
        ctrGenerator.Emit(OpCodes.Stfld, delegateField);

        var metBuilder = typeBuilder.DefineMethod(
            interfaceMethod.Name,
            MethodAttributes.Public | MethodAttributes.Virtual |
                MethodAttributes.Final | MethodAttributes.HideBySig |
                MethodAttributes.NewSlot,
            interfaceMethod.ReturnType,
            interfaceMethod.GetParameters()
                .Select(p => p.ParameterType).ToArray());

        var metGenerator = metBuilder.GetILGenerator();
        metGenerator.Emit(OpCodes.Ldarg_0);
        metGenerator.Emit(OpCodes.Ldfld, delegateField);

        // Generate code to load each parameter.
        byte paramIndex = 1;
        foreach (var param in interfaceMethod.GetParameters())
        {
            metGenerator.Emit(OpCodes.Ldarg_S, paramIndex);
            paramIndex++;
        }
        metGenerator.EmitCall(
            OpCodes.Callvirt,
            funcTypes[methodIndex].GetMethod("Invoke"),
            null);

        metGenerator.Emit(OpCodes.Ret);
        methodIndex++;
    }

    ctrGenerator.Emit(OpCodes.Ret);

    // Add interface implementation and finish creating.
    typeBuilder.AddInterfaceImplementation(interfaceType);
    var wrapperType = typeBuilder.CreateType();
    //assBuilder.Save(aName.Name + ".dll"); // to get a DLL

    return wrapperType;
}

Вы можете использовать это, например,

public interface ITest
{
    void M1();
    string M2(int m2, string n2);
    string prop { get; set; }

    event test BoopBooped;
}

Type it = GenerateInterfaceImplementation<ITest>();
ITest instance = (ITest)Activator.CreateInstance(it,
    new Action(() => {Console.WriteLine("M1 called"); return;}),
    new Func<int, string, string>((i, s) => "M2 gives " + s + i.ToString()),
    new Func<String>(() => "prop value"),
    new Action<string>(s => {Console.WriteLine("prop set to " + s);}),
    new Action<test>(eh => {Console.WriteLine(eh("handler added"));}),
    new Action<test>(eh => {Console.WriteLine(eh("handler removed"));}));

// or with the generated DLL
ITest instance = new DynamicITestWrapper(
    // parameters as before but you can see the signature
    );

Ответ 8

Интересная идея, я был бы немного обеспокоен тем, что даже если это можно было бы сделать, это может запутать. Например. при определении свойства с нетривиальными сеттерами и геттерами, или как устранить ошибки Foo, если тип объявления также содержит свойство Foo.

Интересно, будет ли это проще в более динамичном языке или даже с динамическим типом и DLR в С# 4.0?

Возможно, сегодня на С# некоторые из намерений могут быть достигнуты с помощью lambdas:

void Main() {
    var foo = new Foo();
    foo.Bar = "bar";
    foo.Print = () => Console.WriteLine(foo.Bar);
    foo.Print();
}


class Foo : IFoo {
    public String Bar { get; set; }    
    public Action Print {get;set;}
}

Ответ 9

В настоящее время это невозможно.

Какая разница между этим и просто созданием IFoo конкретного класса? Похоже, это лучший вариант.

Что нужно? Новый компилятор и тонны проверок, чтобы они не нарушили другие функции. Лично я считаю, что проще просто потребовать от разработчиков просто создать конкретную версию своего класса.

Ответ 10

Я использовал в Java класс Amonimous через "новый IFoo() {...}" sintax, и это практично и легко, когда вам нужно быстро реализовать простой интерфейс.

В качестве примера было бы неплохо реализовать IDisposable таким образом на устаревшем объекте, который использовался только один раз, вместо того, чтобы выводить новый класс для его реализации.