【问题标题】:Reflection - SetValue from deep context反射 - 来自深层上下文的 SetValue
【发布时间】:2021-05-31 11:48:12
【问题描述】:

在尝试基于 Json 文件设置“复杂”类层次结构时,我遇到了一个问题,这肯定是由于我在反射过程中缺乏知识。

这是我的主要模型:

public class Names
{
    public Weapons Weapons { get; set; }
    public Armors Armors { get; set; }
    public Utilities Utilities { get; set; }
    public Names()
    {
        Weapons = new Weapons();
        Armors = new Armors();
        Utilities = new Utilities();
    }
}

他们每个人都有一个这样的子模型列表:

public class Weapons
{
    public BattleAxe BattleAxe { get; set; } = new BattleAxe();
    public Bomb_Missile Bomb_Missile { get; set; } = new Bomb_Missile();
    // etc... Around 20 to 25
}

最后是最终模型,它与每个 json 文件完全等效,但可能具有非常不同的属性:

public class BattleAxe
{
    public string[] Normal { get; set; } = new string[0];
    public string[] DescriptiveAdjective { get; set; } = new string[0];
    public string[] Material { get; set; } = new string[0];
    public string[] Type { get; set; } = new string[0];
    public string[] Title { get; set; } = new string[0];
    public string[] Of { get; set; } = new string[0];
    public string[] NormalForTitle { get; set; } = new string[0];
}

由于 MS Json 反序列化器以前不支持像 Newtonsoft 那样转换为 $type,因此我也尝试像这样使用反射填充值(我已经删除了所有代码可读性的空值检查):

public static void Load()
{
    Names = new Names();
    foreach (var category in Names.GetType().GetProperties())
    {
        if (category is not null && !(category.GetGetMethod()?.IsStatic ?? false))
        {
            var categoryType = category.PropertyType;
            foreach (var item in category.PropertyType.GetProperties())
            {
                var itemType = item.PropertyType;
                var subTypeData = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(File.ReadAllText($"./Assets/Names/{categoryType.Name}/{itemType.Name}.json"));
                var concreteObj = Activator.CreateInstance(itemType);
                foreach (var key in subTypeData.Keys)
                {
                    if (itemType.GetProperty(key) is not null && concreteObj is not null)
                    {
                        var prop = concreteObj.GetType().GetProperty(key);
                        var convertedValue = ConvertJsonType(subTypeData[key], subTypeData[key].ValueKind, out var isReferenceType);
                        // It fails here
                        prop.SetValue(
                            isReferenceType ? convertedValue : null,
                            !isReferenceType ? convertedValue : null
                        );
                    }
                }
                item.SetValue(concreteObj, null);
            }
        }
    }
}

因此,它在层次结构中最深对象的prop.SetValue(...) 处失败,并根据要设置的值的类型出现不同的错误。 如果是引用,则会抛出 System.Reflection.TargetException : 'Object does not match target type' Exception 如果它是值,它会抛出一个System.Reflection.TargetException : 'Non-static method requires a target.' 知道我在反序列化方面没有问题,如此处所示,只有我使用动态类型的事实(我的直觉告诉我这实际上是问题......)

我没有添加 ConvertJsonType(...) 正文,因为它很实用且非常简单

我对“为什么”比“如何”更感兴趣,所以如果你能解释一下问题背后的“理论”,那会很有帮助:)

谢谢!

PS:我知道我可以以更易读/更高效的方式简化事情,但我必须通过反思来实现它以供个人学习:) System.Text.Json 命名空间也是如此,我不打算为此切换回 Newtonsoft

【问题讨论】:

    标签: c# dynamic reflection .net-5 system.text.json


    【解决方案1】:

    当调用SetValue(instance, value) 时,您应该传递应该设置属性的对象。

    这是一个疯狂的猜测,但你可以试试这个:

    prop.SetValue(concreteObj,
                  !isReferenceType ? convertedValue : null);
    

    因为你要填充concreteObj的属性,而不是它自身的值。

    如果您查看对象prop,它的返回值是concreteObj.GetType().GetProperty(key);。如果你仔细看,GetProperty 是来自Type 的方法,它没有绑定到任何实例。所以这就是为什么你需要将对象的实例作为第一个参数传递。


    我的意思是积极的:itemType.GetProperty(key) 每次迭代都会被调用,每次迭代都会是相同的值,你可以把它放在循环之前。

    【讨论】:

    • 与 Guru 的答案相同,因此确实是对 SetValue 的错误使用。我只是觉得他的解释对我更有吸引力,但这只是风格问题,两者都是正确的解决方案,谢谢 :) 关于您的 itemType.GetProperty(key) 评论,我如何才能在循环之前获得它作为它的目的检查我正在使用的 json 对象中的当前 Key 是否正确存在于我的模型类中?我知道它的成本很高,但我没有任何简单的想法来减少它的使用
    • 只有我的答案在他之前。至少你找到了。
    【解决方案2】:

    docs 状态 TargetException 在以下情况下被抛出:

    obj 的类型与目标类型不匹配,或者属性是实例属性但objnull

    当您尝试为静态属性而不是实例设置值时,在SetValue 中为obj 传递null 是有效的。作为引用的属性类型与作为实例或静态的属性无关,因此您的调用应如下所示:

    prop.SetValue(concreteObj, convertedValue);
    

    您的item.SetValue(concreteObj, null); 看起来也不正确,因为concreteObj 应该是此调用中的第二个参数。像这样的:

    item.SetValue(Names, concreteObj);
    

    此外,如果您只需要实例属性,您可以提供 BindingFlags 以仅获取实例属性:

    foreach (var category in Names.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
    

    我还要说category is not null 检查是多余的,因此在提供BindingFlags 的同时,您应该完全删除if

    【讨论】:

    • 确实是对 SetValue 函数的误解,愚蠢的错误对不起:/也感谢您使用 BindingFlags,这样更优雅。正如您所说,item.SetValue(concreteObj, null); 存在问题,但我需要设置类别类而不是名称类。因为我还没有任何参考,所以我不得不在 Names 上使用 GetValue(又一个反射调用......)。谢谢你的解释和提示:)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-26
    • 2013-12-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多