C#修改匿名类型中的属性值

咨询区

  • Leo Vo

我有下面一段代码:

var output = new
{
    NetSessionId = string.Empty
};
foreach (var property in output.GetType().GetProperties())
{
    property.SetValue(output, "Test", null);
}

.代码运行后,它会抛出如下异常:

Property set method not found

我想知道如何给这个 匿名类型 的属性赋值?

回答区

  • Alex

从 MSDN :https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/types/anonymous-types 的描述来看,理论上 匿名类型 是不可变的,一旦定义好之后,你是无法对它重新赋值。

但我想提醒的是,其实并没有所谓永恒的不可变,你要是真想变,肯定是有办法的,比如下面的 匿名类

            var myAnonInstance = new
            {
                FirstField = "Hello",
                AnotherField = 30,
            };

当你用 ILSpy 反编译后代码如下:

        internal sealed class <>f__AnonymousType0<<FirstField>j__TPar, <AnotherField>j__TPar>
        {
         [DebuggerBrowsable(DebuggerBrowsableState.Never)]
            private readonly <FirstField>j__TPar<FirstField> i__Field;

            [DebuggerBrowsable(DebuggerBrowsableState.Never)]
            private readonly <AnotherField>j__TPar<AnotherField> i__Field;

            public <FirstField>j__TPar FirstField
            {
                get
                {
                    return < FirstField > i__Field;
                }
            }

            public <AnotherField>j__TPar AnotherField
            {
                get
                {
                    return < AnotherField > i__Field;
                }
            }
        }

可以看到,底层的字段其实是有默认规范的: <xxxxx>i__Field, 这里的 xxxxx 就是属性名字,接下来就可以用 反射 来修改背后的字段即可,参考代码如下:

public static class AnonymousObjectMutator
{
    private const BindingFlags FieldFlags = BindingFlags.NonPublic | BindingFlags.Instance;
    private static readonly string[] BackingFieldFormats = { "<{0}>i__Field", "<{0}>" };

    public static T Set<T, TProperty>(
        this T instance,
        Expression<Func<T, TProperty>> propExpression,
        TProperty newValue) where T : class
    {
        var pi = (propExpression.Body as MemberExpression).Member;
        var backingFieldNames = BackingFieldFormats.Select(x => string.Format(x, pi.Name)).ToList();
        var fi = typeof(T)
            .GetFields(FieldFlags)
            .FirstOrDefault(f => backingFieldNames.Contains(f.Name));
        if (fi == null)
            throw new NotSupportedException(string.Format("Cannot find backing field for {0}", pi.Name));
        fi.SetValue(instance, newValue);
        return instance;
    }
}

然后你可以这样使用。

public static void Main(params string[] args)
{
    var myAnonInstance = new { 
        FirstField = "Hello", 
        AnotherField = 30, 
    };
    Console.WriteLine(myAnonInstance);

    myAnonInstance
        .Set(x => x.FirstField, "Hello SO")
        .Set(x => x.AnotherField, 42);
    Console.WriteLine(myAnonInstance);
}

输出结果:

{ FirstField = Hello, AnotherField = 30 }
{ FirstField = Hello SO, AnotherField = 42 }

点评区

这个题目其实很有意思,虽然语言和框架设计者用了各种限制来阻止我们做一些事情,其实都有化解的方法,所以并没有永恒的不可变,最彻底的还可以通过修改内存地址变更,不是嘛~