|
Динамическая реализация интерфейсов (C#)
| |
Предположим, есть интерфейс:
C#
public interface ITest
{
DateTime CurDate { get; set; }
String Name { get; set; }
MyClass ClassID { get; set; }
void test1(string s1, DateTime d1, int i1, int i2, int i3);
}
|
Основное назначение этого интерфейса - описание структуры данных, содержащей
время, имя, и значение некоторого нашего класса. (Про дополнительный метод
test1 будет сказано ниже).
Для тестирования создадим простой класс, хранящий целочисленный
идентификатор:
C#
public class MyClass
{
public MyClass(int ID) { this.id = ID; }
public override string ToString()
{
return id.ToString();
}
private int id = 0;
}
|
Предположим, что данные хранятся и обрабатываются другим классом - исполнителем.
Назовем его DataObject. Два метода этого класса позволяют получить и установить
значение по его имени. Напишем простую реализацию такого класса. Заодно,
объявим интерфейс IDataObject, чтобы явно указать, что наш класс обрабатывает
методы getObject и setObject.
C#
interface IDataObject
{
object getObject(string name);
void setObject(string name, object value);
}
public class DataObject : IDataObject
{
private Dictionary<string, object> list = new Dictionary<string, object>();
public DataObject()
{
Console.WriteLine("DataObject.constructor");
list.Add("CurDate", DateTime.Now);
list.Add("Name", "test string");
list.Add("ClassID", new MyClass(1));
}
public object getObject(string name)
{
Console.WriteLine("getObject: "+name);
return list[name];
}
public void setObject(string name, object value)
{
Console.WriteLine("setObject: "+name+ " = "+ value);
list[name] = value;
}
public void test1(string s1, DateTime d1, int i1, int i2, int i3)
{
ITest t1 = (ITest)this;
t1.Name = s1;
t1.CurDate = d1;
Console.WriteLine(i1);
Console.WriteLine(i2);
Console.WriteLine(i3);
}
}
|
Посмотрим как бы выглядела реализация интерфейса ITest, если бы мы писали ее
сами с использованием класса DataObject:
C#
class TestDataObject : DataObject, ITest
{
public DateTime CurDate
{
get { return (DateTime)getObject("CurDate"); }
set { setObject("CurDate", value); }
}
public string Name
{
get { return (string)getObject("Name"); }
set { setObject("Name", value); }
}
public MyClass ClassID
{
get { return (MyClass)getObject("ClassID"); }
set { setObject("ClassID", value); }
}
new public void test1(string s1, DateTime d1, int i1, int i2, int i3)
{
base.test1(s1, d1, i1, i2, i3);
}
}
|
Как видим описание всех свойств выглядят одинаково - обращение к методу
getObject исполнителя для получения значения и к методу setObject для
записи значения. Этим мы и воспользуемся для динамической реализации
интерфейса. Исполнителя мы сделали предком нашего класса. Во-первых это
позволяет использовать интерфейс IDataObject без включения дополнительных
объектов, а во-вторых, значительно упрощает реализацию обычных методов
интерфейса. В ITest объявлен метод test1. В реализации этого метода
вызов просто перенаправляется исполнителю. В исполнителе настоящая реализация
метода и, поскольку, сам исполнитель в данном случае реализует интерфейс
ITest, допустимо приведение к типу ITest и обращение к свойствам интерфейса.
Теперь уберем из проекта класс TestDataObject и будем создавать подобный класс
динамически. Еще раз. У нас есть интерфейс ITest, есть исполнитель, DataObject,
который реализует получение/установку свойств и вызовы методов этого
интерфейса. Требуется создать экземпляр класса, который был-бы реализацией
нашего интерфейса (ITest), но все вызовы перенаправлял исполнителю.
Напишем статический класс Generator, единственный доступный метод
класса CreateInstance будет создавать такой экземпляр. Этот метод
создает новую сборку с типом, реализующим интерфейс и унаследованным от
исполнителя. Для этого типа создаются заглушки для всех методов,
описанных в интерфейсе. Вот код такого класса:
C#
using System.Collections;
using System.Reflection;
using System.Reflection.Emit;
//...
static class Generator
{
private static TypeBuilder CreateTypeBuilder(Type intf, Type parent)
{
AssemblyName a = new AssemblyName();
a.Name = Guid.NewGuid().ToString();
AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly(a, AssemblyBuilderAccess.RunAndSave);
ModuleBuilder mb = ab.DefineDynamicModule(a.Name + ".dll");
TypeBuilder tb = mb.DefineType(intf.Name + "_implementation", TypeAttributes.Public, parent);
tb.AddInterfaceImplementation(intf);
return tb;
}
private static Type Generate(Type T, Type parent)
{
TypeBuilder tb = CreateTypeBuilder(T, parent);
ConstructorBuilder cb = tb.DefineDefaultConstructor(
MethodAttributes.Assembly | MethodAttributes.HideBySig);
foreach (MethodInfo method in T.GetMethods())
{
MethodBuilder mb = CreateMethodBuilder(tb, method);
if (method.Name.StartsWith("get_"))
{
CreateGetMethodBody(parent, mb, method);
}
else if (method.Name.StartsWith("set_"))
{
CreateSetMethodBody(parent, mb, method);
}
else
{
CreateOtherMethodBody(parent, mb, method);
}
//Console.WriteLine(method.Name);
}
return tb.CreateType();
}
private static MethodAttributes _methodOverrideAttrs =
MethodAttributes.Public | MethodAttributes.HideBySig |
MethodAttributes.NewSlot | MethodAttributes.Virtual |
MethodAttributes.Final;
private static MethodBuilder CreateMethodBuilder(TypeBuilder tb, MethodInfo method)
{
ArrayList paramTypes = new ArrayList();
foreach (ParameterInfo param in method.GetParameters())
paramTypes.Add(param.ParameterType);
MethodBuilder mb = tb.DefineMethod(
method.Name,
_methodOverrideAttrs,
method.ReturnType,
(Type[])paramTypes.ToArray(typeof(Type)));
int paramIndex = 1;
foreach (ParameterInfo param in method.GetParameters())
mb.DefineParameter(paramIndex++, param.Attributes, param.Name);
return mb;
}
private static void CreateGetMethodBody(Type parent, MethodBuilder mb, MethodInfo method)
{
ILGenerator g = mb.GetILGenerator();
g.Emit(OpCodes.Ldarg_0);
g.Emit(OpCodes.Ldstr, method.Name.Substring(4));
g.EmitCall(OpCodes.Call, parent.GetMethod("getObject"), new Type[] { });
Type returnType = method.ReturnType;
if (returnType != typeof(void))
{
if (returnType.IsValueType)
g.Emit(OpCodes.Unbox_Any, returnType);
else
g.Emit(OpCodes.Castclass, returnType);
}
g.Emit(OpCodes.Ret);
}
private static void CreateSetMethodBody(Type parent, MethodBuilder mb, MethodInfo method)
{
ILGenerator g = mb.GetILGenerator();
g.Emit(OpCodes.Ldarg_0);
g.Emit(OpCodes.Ldstr, method.Name.Substring(4));
g.Emit(OpCodes.Ldarg_1);
Type param2type = method.GetParameters()[0].ParameterType;
if (param2type.IsValueType)
g.Emit(OpCodes.Box, param2type);
g.EmitCall(OpCodes.Call, parent.GetMethod("setObject"), new Type[] { });
g.Emit(OpCodes.Ret);
}
private static void CreateOtherMethodBody(Type parent, MethodBuilder mb, MethodInfo method)
{
ILGenerator g = mb.GetILGenerator();
g.Emit(OpCodes.Ldarg_0);
int C = method.GetParameters().Length;
for (int i = 1; i <= C; i++)
{
switch (i)
{
case 1: { g.Emit(OpCodes.Ldarg_1); break; }
case 2: { g.Emit(OpCodes.Ldarg_2); break; }
case 3: { g.Emit(OpCodes.Ldarg_3); break; }
default: { g.Emit(OpCodes.Ldarg_S, i); break; }
}
}
g.EmitCall(OpCodes.Call, parent.GetMethod(method.Name), new Type[] {typeof(string)});
Type returnType = method.ReturnType;
if (returnType != typeof(void))
{
if (returnType.IsValueType)
g.Emit(OpCodes.Unbox_Any, returnType);
else
g.Emit(OpCodes.Castclass, returnType);
}
g.Emit(OpCodes.Ret);
}
public static T CreateInstance<T, T1>()
where T : class
where T1 : IDataObject
{
return (T)Activator.CreateInstance(
Generate(typeof(T), typeof(T1)),
BindingFlags.NonPublic | BindingFlags.Instance,
null, null, null);
}
}
|
И, наконец, протестируем создание экземпляра и вызовы методов
C#
// Создаем экземпляр класса, реализующего интерфейс ITest
// используя как исполнитель класс DataObject
ITest t2 = Generator.CreateInstance<ITest, DataObject>();
// Обращение к свойствам интерфейса возврщают значения,
// которые мы указали в констракторе DataObject:
Console.WriteLine(t2.Name); // test string
Console.WriteLine(t2.CurDate); // текущая дата
Console.WriteLine(t2.ClassID); // ClassID = 1
// Можно изменить эти свойства
t2.Name = "test";
t2.CurDate = DateTime.Now.AddYears(1);
t2.ClassID = new MyClass(5);
// Доступен и вызов метода
t2.test1("test string 2", DateTime.Now.AddYears(1), 1, 2, 3);
// В реализации метода test1 мы меняем значение t2.Name
Console.WriteLine(t2.Name); // test string 2
|
|