通过反射或类似方法动态生成属性的getter/setter函数

5

想象一下以下的类:

public class Settings
{
    [FileBackedProperty("foo.txt")]
    public string Foo { get; set; }
}

我希望能够编写类似上述的代码,并从文件“foo.txt”中读取settings.Foo,同时将settings.Foo =“bar”写入“foo.txt”中。显然,这只是一个简化的示例,在生产应用程序中不会像上述那样操作。但还有其他示例,例如如果我想要将Foo存储在ASP.net会话状态“foo”中,但我厌倦了一遍又一遍地编写以下代码:
public int Foo
{
    get
    {
        if (Session["foo"] != null)
            return Convert.ToInt32(Session["foo"]);
        else
            // Throw an exception or return a default value
    }
    set
    {
        Session["foo"] = value;
    }
}

再次强调,这个例子是简化的,实际上我并不会写上面的代码,事实上我在尝试重构它,因此才有了这个问题。

上面的例子很好,除非你有50个不同的会话值,所有这些值都有类似的逻辑。那么有没有办法将第二个属性转换成类似于第一个属性的东西呢?(使用属性和反射,或者其他方法?)


如果您正在使用Visual Studio,您应该查看code snippets。它们允许您输入一些简短的内容,扩展到所需的代码,然后允许您填写指定的区域。类似于现有的propg等功能。 - Joshua Drake
1
@JoshuaDrake 这似乎和复制/粘贴一样糟糕。如果我想要更改实现,我仍然必须逐个更改每个属性。 - thelsdj
那么,您想在运行时动态添加属性吗?您是否拥有所有服务器机器或对性能要求不高?虽然您可以这样做,但我强烈建议不要这样做。如果您并不是指在运行时添加属性,那么您仍然需要重新生成任何代码输出。 - Joshua Drake
6个回答

6
我知道这不是你(也不是我)需要的,但这是最接近的解决方案而不使用第三方库。你可以改变get和set方法的逻辑,并为GetProperty和GetCustomAttributes方法添加一些缓存,或者如果你已经有一个基类,可以将get和set方法编写为帮助类中的静态方法。再次强调,这不是完美的答案,可能性能不佳,但至少减少了你复制和粘贴的代码(:

注意:重要的是使属性虚拟以防止编译器内联它们。

public class SampleClass : SessionObject
{
    [Session(Key = "SS_PROP")]
    public virtual int SampleProperty
    {
        get { return get(); }
        set { set(value); }
    }

    [Session(Key = "SS_PROP2")]
    public virtual string SampleProperty2
    {
        get { return get(); }
        set { set(value); }
    }
}

[AttributeUsage(AttributeTargets.Property)]
public class SessionAttribute : Attribute
{
    public string Key { get; set; }
}

public abstract class SessionObject
{
    Dictionary<string, object> Session = new Dictionary<string, object>();

    protected void set(object value)
    {
        StackFrame caller = new StackFrame(1);
        MethodBase method = caller.GetMethod();
        string propName = method.Name.Substring(4);
        Type type = method.ReflectedType;
        PropertyInfo pi = type.GetProperty(propName);
        object[] attributes = pi.GetCustomAttributes(typeof(SessionAttribute), true);
        if (attributes != null && attributes.Length == 1)
        {
            SessionAttribute ssAttr = attributes[0] as SessionAttribute;
            Session[ssAttr.Key] = value;
        }
    }

    protected dynamic get()
    {
        StackFrame caller = new StackFrame(1);
        MethodBase method = caller.GetMethod();
        string propName = method.Name.Substring(4);
        Type type = method.ReflectedType;
        PropertyInfo pi = type.GetProperty(propName);
        object[] attributes = pi.GetCustomAttributes(typeof(SessionAttribute), true);
        if (attributes != null && attributes.Length == 1)
        {
            SessionAttribute ssAttr = attributes[0] as SessionAttribute;
            if (Session.ContainsKey(ssAttr.Key))
            {
                return Session[ssAttr.Key];
            }
        }
        return default(dynamic);
    }
}

3
另一种选择是使用PostSharp。您可以定义属性,它会在最终代码中注入IL,因此不会更改源代码。这既有好处,也有坏处。

但这个产品不是免费的。

一些入门提示

希望这能帮到您。


你提到它注入了IL,这是在编译时还是应用程序启动时进行的?我猜测PostSharp绕过了使用ContextBoundObject的AOP所遇到的性能问题? - thelsdj
这是编译时。自然而然,没有好处就没有问题 :) 我认为主要问题是最终运行的二进制代码与您的代码看起来期望的不同。当然,也存在一些性能问题,但这需要在具体实现上进行衡量。 - Tigran
我是说这是一款工业级软件,值得关注。 - Tigran
看起来在这里:http://www.sharpcrafters.com/purchase/compare,我可能可以使用免费的“Starter”版本来完成我需要的工作。我可能会尝试一下,看看是否可行。 - thelsdj
@thelsdj:这是个好主意。是的,应该有免费试用版。 - Tigran

1

如果想要避免写太多的 getter 代码,可以编写一个辅助方法:

public int Foo
{
    get
    {
        return GetHelper<int>("foo");
    }
    set
    {
        Session["foo"] = value;
    }
}

public T GetHelper<T>(string name, T defaultValue = default(T))
{
    if (Session[name] != null)
        return (T)Session[name];
    else
    {
        return defaultValue;
    }
}

如果您可以访问动态对象,那么您可以使用动态对象来包装会话:
internal class DynamicSession : DynamicObject
{
    private HttpSessionState_session;

    public DynamicSession()
    {
        _session = HttpContext.Current.Session;
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        if (_session[binder.Name] != null)
        {
            result = _session[binder.Name];
            return true;
        }
        result = null;
        return false;
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        _session[binder.Name] = value;
        return true;
    }
}

然后你可以像这样使用它:

dynamic session = new DynamicSession();
    //These properties are "magically" put in and taken out of session!
//get
int foo = session.Foo;
//set
session.Foo = 3;

最后一个选项就是像Resharper中的Live Templates这样的工具,可以让你输入代码变得更加轻松。

@Tigran 是的,但也许这是不可能的。上面的代码确实更短,重复性更少。看起来很遗憾,因为没有一个好的方法可以做到我想要的,这会消除大量的重复。 - thelsdj
@Tigran 我包含了一段利用DLR和动态语言实现这些结果的代码片段。 - vcsjones
@vcsjones 动态版本真的很好,除了我现在使用的是3.5版本,而且我还需要能够控制属性名称到会话变量转换(虽然可以通过查找表或其他方式来完成)。对于其他人有这个选项的人来说,+1。 - thelsdj

1

1

你正在尝试做的是“面向方面编程”,在某些需要重复使用代码但只有非常小的变化时,这种方法相对常见。你的例子肯定符合这个条件。

基本思想如下:创建一个属性,用于装饰类或类成员。该属性为CLR中的消息传递系统定义了一个“上下文”,允许您将方法拦截器挂钩到调用它时运行的方法上。

请注意,这会带来显著的性能损失;具有由属性装饰的成员的对象必须继承自MarshallByRefObject或ContextBoundObject之一;即使您实际上没有进行任何属性装饰,其中任何一个都会在运行时期间使对象的性能降低约10倍。

以下是一些示例代码:http://www.developerfusion.com/article/5307/aspect-oriented-programming-using-net/3/

您还可以使用动态代理基于属性装饰或其他反射信息“即时”创建对象。这是许多C#开发人员认为理所当然的技术背后的原理,如ORM,IoC框架等等。您基本上会使用像Castle DynamicProxy这样的工具创建一个看起来像基本对象的对象,但具有用文件进行填充/持久化逻辑的属性的覆盖定义。

0

你也可以使用Castle.Core的DynamicProxy nuget包来实现这种行为。

你可以拦截对类的所有虚属性的Get和Set方法的调用。但是,你想要修改的所有属性的getter和setter必须是虚拟的。

我在这里提供了一个更完整的答案:https://stackoverflow.com/a/48764825/5103354 并且这里有一个gist可用:here

应该观察到以下行为:

    [Fact]
    public void SuccessFullyRegisterGetAndSetEvents()
    {
        ProxyGenerator generator = new ProxyGenerator();
        var tracked = generator.CreateClassProxy<TrackedClass>(new GetSetInterceptor());
        tracked.SomeContent = "some content";
        Assert.Single(tracked.GetEvents());
        var eventAfterSomeContentAssigned = tracked.GetEvents().Last();
        Assert.Equal(EventType.Set, eventAfterSomeContentAssigned.EventType);
        Assert.Equal("some content", eventAfterSomeContentAssigned.Value);
        Assert.Equal("SomeContent", eventAfterSomeContentAssigned.PropertyInfo.Name);
        tracked.SomeInt = 1;
        Assert.Equal(2, tracked.GetEvents().Count);
        var eventAfterSomeIntAssigned = tracked.GetEvents().Last();
        Assert.Equal(EventType.Set, eventAfterSomeContentAssigned.EventType);
        Assert.Equal(1, eventAfterSomeIntAssigned.Value);
        Assert.Equal("SomeInt", eventAfterSomeIntAssigned.PropertyInfo.Name);
        var x = tracked.SomeInt;
        Assert.Equal(3, tracked.GetEvents().Count);
        var eventAfterSomeIntAccessed = tracked.GetEvents().Last();
        Assert.Equal(EventType.Get, eventAfterSomeIntAccessed.EventType);
        Assert.Equal(1, eventAfterSomeIntAccessed.Value);
        Assert.Equal("SomeInt", eventAfterSomeIntAccessed.PropertyInfo.Name);
    }

希望这能有所帮助。

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