C#如何通过反射初始化属性(没有setter)

C# How to init property (without setter) by reflection

任务: 使用 protobuf 将对象列表序列化为 byte[]。

不反思就好

.proto

message MyObject{
  int32 id = 1;
  int32 value = 2;
}

message MyObjects {
  repeated MyObject objects = 1;
}

.cs

public static byte[] ToByteArray(List<MyObject> obj) {
    var objects = new MyObjects {
        Objects = {obj}
    };
    return objects.ToByteArray();
} 

因为我需要用这种方式序列化很多不同的类型,所以我想用反射写一个通用的方法。

问题: Protobuf 本身为它们生成实体和属性,但它不会为 RepeatedField 创建 setter,这意味着我无法使用 GetProperty("Objects")?.SetValue(objects, obj) 设置值。 System.ArgumentException:找不到 'Objects'

的设置方法

.cs(生成的 protobuf)

public pbc::RepeatedField<global::Test.MyObject> Objects {
  get { return objects_; }
}

.cs

public static byte[] ToByteArray<T, E>(List<T> obj) where T : IMessage where E : IMessage {
    var objects = Activator.CreateInstance<E>();
    objects.GetType().GetProperty("Objects")?.SetValue(objects, obj);
    return objects.ToByteArray();
} 

问题: 如何在创建对象时使用反射给a属性赋值,就像我没有反射一样?

如何使用反射编写这个“new MyObjects {Objects = {obj}};(其中 obj:IEnumerable)”

各种结论:

当我们这样做时:

var x = new Thing
{
    SomeProperty = "x",
    SomeOtherProperty = 1
}

我们没有在对象创建期间设置值。这相当于:

var x = new Thing();
x.SomeProperty = "x";
x.SomeOtherProperty = 1;

在这两种情况下,属性都是在 对象通过设置其属性实例化之后设置的。验证这一点的一种简单方法是尝试使用第一个示例中的语法来设置没有 setter 的 属性。它不会编译。您会看到此错误:

Property or indexer 'Thing.SomeProperty' cannot be assigned to -- it is read-only.

换句话说,定义的对象不提供设置 Objects 属性.

的方法

问题是你是否真的需要设置属性。您很可能只需要将项目添加到集合中。

用反射做这个还是很丑。我根本不推荐这个。这是一个粗略的版本。由于各种原因,它可能会在运行时失败。

public static byte[] ToByteArray<T, E>(List<T> itemsToAdd) where T : IMessage where E : IMessage
{
    // create an instance of the object
    var created = Activator.CreateInstance<E>();

    // Find the "Objects" property. It could be null. It could be the wrong type.
    var objectsProperty = typeof(E).GetProperty("Objects"); 

    // Get the value of the objects property. Hopefully it's the type you expect it to be.
    var collection = objectsProperty.GetValue(created);

    // Get the Add method. This might also be null if the method doesn't exist.
    var addMethod = collection.GetType().GetMethod("Add");

    // invoke the Add method for each item in the collection
    foreach(var itemToAdd in itemsToAdd)
    {
        addMethod.Invoke(collection, new object[] { itemToAdd });
    }
    return created.ToByteArray();
}

除非迫不得已,否则我们真的不想那样做。我不知道你的 IMessage 类型是什么样的。

它有 Objects 属性 吗?

在这种情况下,您可以这样做:

public static byte[] ToByteArray<T, E>(List<T> itemsToAdd) 
    where T : IMessage 
    where E : IMessage, new()
{
    var created = new E();
    foreach (var itemToAdd in itemsToAdd)
    {
        created.Objects.Add(itemToAdd);
    }

    // or skip the foreach and just do
    // created.Objects.AddRange(itemToAdd);

    return created.ToByteArray();
}

我在猜测您的界面是否有 属性。但是,如果可能的话,最好使用通用约束而不是反射。这样你的代码在编译时会检查大多数可能的错误,而不是 运行 它并让它爆炸,因为这个或那个 属性 或方法不存在,是错误的,等等

new() 约束只是意味着 E 必须是具有默认构造函数的类型,这意味着为了使其能够编译,E 必须是您可以创建而不向构造函数传递任何内容。 (没有这个约束,new E() 将无法编译。)

如果没有该约束,甚至 Activator.CreateInstance 也可能会失败,因为该类型可能没有默认构造函数。

Scott's 解决了问题,但我最后使用了一个缩短的解决方案

    private static byte[] ToByteArray<T, E>(IEnumerable<T> obj) where T : IMessage where E : IMessage, new() {
        var objects = new E();
        (objects.GetType().GetProperty("Objects")?.GetValue(objects) as RepeatedField<T>)?.AddRange(obj);
        return objects.ToByteArray();
    }