如何通过添加额外属性扩展类

14

假设我有一个名为Foo的类。

我无法更改Foo类,但我想使用类型为string的属性Bar扩展它。

同时,我有很多像Foo这样的类,因此我需要一种“通用”的解决方案。

我正在研究ExpandoObjectdynamic,并得到了我要求的结果,但我想知道是否可以在不使用dynamic的情况下完成...

static void Main(string[] args)
{
    var foo = new Foo() { Thing = "this" };
    var fooplus = Merge(foo, new { Bar = " and that" });
    Console.Write(string.Concat(fooplus.Thing, fooplus.Bar));
    Console.ReadKey();
}

public class Foo
{
    public string Thing { get; set; }
}

public static dynamic Merge(object item1, object item2)
{
    if (item1 == null || item2 == null)
    return item1 ?? item2 ?? new ExpandoObject();

    dynamic expando = new ExpandoObject();
    var result = expando as IDictionary<string, object>;
    foreach (System.Reflection.PropertyInfo fi in item1.GetType().GetProperties())
    {
        result[fi.Name] = fi.GetValue(item1, null);
    }
    foreach (System.Reflection.PropertyInfo fi in item2.GetType().GetProperties())
    {
        result[fi.Name] = fi.GetValue(item2, null);
    }
    return result;
}

1
为什么不使用扩展方法呢? - John Alexiou
部分类?(http://msdn.microsoft.com/en-us/library/wa80x488(v=vs.90).aspx) - Liam
3
@Liam,我认为“我无法更改Foo类”也意味着他不能将其标记为“partial”。 - dcastro
1
如果“Foo”未被密封,您可以对其进行扩展,例如“public class FooBar:Foo { public string Bar { get; set; } }”。 - Tim S.
1
好的,我想你将不得不继续使用当前的方法。但要注意性能影响。 - dcastro
显示剩余14条评论
4个回答

17

您的问题可以通过使用Reflection.Emit和运行时代码生成相对容易地解决。

现在假设您有以下类需要扩展。

public class Person
{
    public int Age { get; set; }
}

这个类代表一个人,其中包含一个名为Age的属性,用来表示这个人的年龄。

在您的情况下,您还想添加一个类型为stringName属性,以表示这个人的姓名。

最简单和最流畅的解决方案是定义以下接口。

public interface IPerson
{   
    string Name { get; set; }
    int Age { get; set; }
}

这个接口将用于扩展您的类,应包含当前类中包含的所有旧属性和您想要添加的新属性。原因很快就会变得清楚。

您现在可以使用以下类定义来实际扩展您的类,通过创建一个新类型,在运行时也使其从上述提到的接口继承。

class DynamicExtension<T>
{
    public K ExtendWith<K>()
    { 
        var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("Assembly"), AssemblyBuilderAccess.Run);
        var module = assembly.DefineDynamicModule("Module");
        var type = module.DefineType("Class", TypeAttributes.Public, typeof(T));

        type.AddInterfaceImplementation(typeof(K));

        foreach (var v in typeof(K).GetProperties())
        {
            var field = type.DefineField("_" + v.Name.ToLower(), v.PropertyType, FieldAttributes.Private);
            var property = type.DefineProperty(v.Name, PropertyAttributes.None, v.PropertyType, new Type[0]);
            var getter = type.DefineMethod("get_" + v.Name, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.Virtual, v.PropertyType, new Type[0]);
            var setter = type.DefineMethod("set_" + v.Name, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.Virtual, null, new Type[] { v.PropertyType });

            var getGenerator = getter.GetILGenerator();
            var setGenerator = setter.GetILGenerator();

            getGenerator.Emit(OpCodes.Ldarg_0);
            getGenerator.Emit(OpCodes.Ldfld, field);
            getGenerator.Emit(OpCodes.Ret);

            setGenerator.Emit(OpCodes.Ldarg_0);
            setGenerator.Emit(OpCodes.Ldarg_1);
            setGenerator.Emit(OpCodes.Stfld, field);
            setGenerator.Emit(OpCodes.Ret);

            property.SetGetMethod(getter);
            property.SetSetMethod(setter);

            type.DefineMethodOverride(getter, v.GetGetMethod());
            type.DefineMethodOverride(setter, v.GetSetMethod());
        }

        return (K)Activator.CreateInstance(type.CreateType());
    }
}

要使用这个类,只需执行以下代码行。
class Program
{
    static void Main(string[] args)
    {
        var extended = new DynamicExtension<Person>().ExtendWith<IPerson>();

        extended.Age = 25;
        extended.Name = "Billy";

        Console.WriteLine(extended.Name + " is " + extended.Age);

        Console.Read();
    }
}

现在你可以看到,我们使用接口来扩展我们新创建的类的原因是为了以类型安全的方式访问其属性。如果我们只返回对象类型,那么我们将被迫通过反射来访问其属性。
编辑:
以下修改后的版本现在能够实例化接口内部的复杂类型,并实现其他简单类型。
Person类的定义保持不变,而IPerson接口现在变成了以下形式。
public interface IPerson
{
    string Name { get; set; }

    Person Person { get; set; }
}

DynamicExtension类的定义现在更改为以下内容。

class DynamicExtension<T>
{
    public T Extend()
    {
        var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("Assembly"), AssemblyBuilderAccess.Run);
        var module = assembly.DefineDynamicModule("Module");
        var type = module.DefineType("Class", TypeAttributes.Public);

        type.AddInterfaceImplementation(typeof(T));

        foreach (var v in typeof(T).GetProperties())
        {
            var field = type.DefineField("_" + v.Name.ToLower(), v.PropertyType, FieldAttributes.Private);
            var property = type.DefineProperty(v.Name, PropertyAttributes.None, v.PropertyType, new Type[0]);
            var getter = type.DefineMethod("get_" + v.Name, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.Virtual, v.PropertyType, new Type[0]);
            var setter = type.DefineMethod("set_" + v.Name, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.Virtual, null, new Type[] { v.PropertyType });

            var getGenerator = getter.GetILGenerator();
            var setGenerator = setter.GetILGenerator();

            getGenerator.Emit(OpCodes.Ldarg_0);
            getGenerator.Emit(OpCodes.Ldfld, field);
            getGenerator.Emit(OpCodes.Ret);

            setGenerator.Emit(OpCodes.Ldarg_0);
            setGenerator.Emit(OpCodes.Ldarg_1);
            setGenerator.Emit(OpCodes.Stfld, field);
            setGenerator.Emit(OpCodes.Ret);

            property.SetGetMethod(getter);
            property.SetSetMethod(setter);

            type.DefineMethodOverride(getter, v.GetGetMethod());
            type.DefineMethodOverride(setter, v.GetSetMethod());
        }

        var instance = (T)Activator.CreateInstance(type.CreateType());

        foreach (var v in typeof(T).GetProperties().Where(x => x.PropertyType.GetConstructor(new Type[0]) != null))
        {
            instance.GetType()
                    .GetProperty(v.Name)
                    .SetValue(instance, Activator.CreateInstance(v.PropertyType), null);
        }

        return instance;
    }
}

我们现在可以简单地执行以下代码行来获取所有适当的值。
class Program
{
    static void Main(string[] args)
    {
        var extended = new DynamicExtension<IPerson>().Extend();

        extended.Person.Age = 25;
        extended.Name = "Billy";

        Console.WriteLine(extended.Name + " is " + extended.Person.Age);

        Console.Read();
    }
}

我对这段代码感到非常兴奋,唯一的缺点是我需要一个与Person和我需要的额外属性匹配的接口。但我可能会让它工作。有趣。 - Ralf de Kleine
1
+1 - 哇哦,这看起来非常有趣,并且暗示着一些很好的替代用途。我非常喜欢它。如果您能将其重构为类似于 OP 合并的方式工作(即不需要在接口中重复基本属性),那将在许多应用程序中非常有用。甚至可能会尝试自己进行重构... - jim tollan
啊 - 注意最终结果是人类扩展的!!嗯嗯 - jim tollan
@RalfdeKleine 我可以编辑我的答案以更好地满足你的需求。你具体想让我改变什么? :D - Mario Stopfer
1
Jim,我可以将这两种类型合并成一个新的类型,这是完全可能的。但是返回的结果类型将是Object类型,因此没有智能感知。为了使用智能感知,我们只需要提前拥有该类型或其接口即可。 - Mario Stopfer
显示剩余8条评论

3

由于我的评论变得非常冗长,所以我想添加一个新答案。这个答案完全是Mario的工作和思考,只有我稍微添加了一些内容来说明我的观点。

对于Mario的示例,有一些小改动可以使其工作得非常好,即将现有属性添加为类对象,而不是复制整个类。无论如何,这就是它的样子(只添加了修改部分,其他都与Mario的答案相同):

public class Person
{
    public int Age { get; set; }
    public string FaveQuotation { get; set; }
}

对于IPerson接口,我们添加了实际的Person类,而不是复制属性:

public interface IPerson
{
    // extended property(s)
    string Name { get; set; }
    // base class to extend - tho we should try to overcome using this
    Person Person { get; set; }
}

这意味着更新后的使用方法:
static void Main(string[] args)
{
    var extended = new DynamicExtension<Person>().ExtendWith<IPerson>();

    var pocoPerson = new Person
    {
        Age = 25,
        FaveQuotation = "2B or not 2B, that is the pencil"
    };

    // the end game would be to be able to say: 
    // extended.Age = 25; extended.FaveQuotation = "etc";
    // rather than using the Person object along the lines below
    extended.Person = pocoPerson;
    extended.Name = "Billy";

    Console.WriteLine(extended.Name + " is " + extended.Person.Age 
        + " loves to say: '" + extended.Person.FaveQuotation + "'");

    Console.ReadKey();
}

希望这可以帮助原始OP,我知道它让我思考,但是还不确定是否应该将Person类扁平化为方法中的相同级别,作为新属性!因此,实际上使用new DynamicExtension<Person>().ExtendWith<IPerson>();这一行应该返回一个完全扩展的新对象-包括Intellisence。这是个难题 - 嗯...

2

如果没有访问类定义的权限,你最好的选择是创建一个从目标类派生的类,除非原始类被封装。


0

我知道这有点晚了。一个NuGet包已经被创建,它抽象出了在运行时扩展类型所需的所有复杂性。它非常简单:

var className = "ClassA";
var baseType = typeof(List<string>);
var typeExtender = new TypeExtender(className, baseType);
typeExtender.AddProperty("IsAdded", typeof(bool));
typeExtender.AddProperty<double>("Length");
var returnedClass = typeExtender.FetchType();
var obj = Activator.CreateInstance(returnedClass);

你可以在仓库TypeExtender上找到更多的使用说明。Nuget包位于nuget


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接