测试动态变量是否存在某个属性

264

我的情况非常简单。在我代码的某个地方,我有这样的一行:

dynamic myVariable = GetDataThatLooksVerySimilarButNotTheSame();

//How to do this?
if (myVariable.MyProperty.Exists)   
//Do stuff

所以,我的问题基本上是如何检查(不抛出异常)动态变量上是否有某个属性可用。我可以使用GetType(),但我宁愿避免这样做,因为我并不需要知道对象的类型。我真正想知道的是是否有一个属性(或方法,如果更容易)可用。有什么提示吗?


1
这里有几个建议:https://dev59.com/-3A85IYBdhLWcg3wBOpO - 但目前还没有被接受的答案。 - Andrew Anderson
谢谢,我可以看到如何制作其中一种解决方案,但我想知道是否有任何我遗漏的东西。 - roundcrisis
可能是重复的问题:如何检测ExpandoObject上是否存在属性? - Sebastian
15个回答

179
我认为没有办法在不尝试访问的情况下找出一个dynamic变量是否有某个成员,除非你重新实现C#编译器中动态绑定的处理方式。这可能需要很多猜测,因为根据C#规范,它是实现定义的。
因此,你应该尝试访问该成员并捕获异常(如果操作失败):
dynamic myVariable = GetDataThatLooksVerySimilarButNotTheSame();

try
{
    var x = myVariable.MyProperty;
    // do stuff with x
}
catch (RuntimeBinderException)
{
    //  MyProperty doesn't exist
} 

2
我会将其标记为答案,因为时间过去这么久了,它似乎是最佳答案。 - roundcrisis
8
更好的解决方案 - https://dev59.com/ZnE85IYBdhLWcg3wUBsZ - ministrymason
24
如果你的意思是把dynamic对象转换成IDictionary来处理,那只有在ExpandoObject上才可以这样做,而其他类型的dynamic对象则不行。 - svick
5
RuntimeBinderException位于Microsoft.CSharp.RuntimeBinder命名空间中。 - DavidRR
12
我仍然觉得,在任何情况下使用 try/catch 代替 if/else 都是一种不好的通用做法,无论具体情境如何。 - Alexander Ryan Baggett
显示剩余12条评论

86
我想比较Martijn的回答svick的回答...
以下程序返回以下结果:
Testing with exception: 2430985 ticks
Testing with reflection: 155570 ticks

void Main()
{
    var random = new Random(Environment.TickCount);

    dynamic test = new Test();

    var sw = new Stopwatch();

    sw.Start();

    for (int i = 0; i < 100000; i++)
    {
        TestWithException(test, FlipCoin(random));
    }

    sw.Stop();

    Console.WriteLine("Testing with exception: " + sw.ElapsedTicks.ToString() + " ticks");

    sw.Restart();

    for (int i = 0; i < 100000; i++)
    {
        TestWithReflection(test, FlipCoin(random));
    }

    sw.Stop();

    Console.WriteLine("Testing with reflection: " + sw.ElapsedTicks.ToString() + " ticks");
}

class Test
{
    public bool Exists { get { return true; } }
}

bool FlipCoin(Random random)
{
    return random.Next(2) == 0;
}

bool TestWithException(dynamic d, bool useExisting)
{
    try
    {
        bool result = useExisting ? d.Exists : d.DoesntExist;
        return true;
    }
    catch (Exception)
    {
        return false;
    }
}

bool TestWithReflection(dynamic d, bool useExisting)
{
    Type type = d.GetType();

    return type.GetProperties().Any(p => p.Name.Equals(useExisting ? "Exists" : "DoesntExist"));
}

因此,我建议使用反射。请参见下文。


回应bland的评论:

比率是100000次迭代的反射:异常刻度:

Fails 1/1: - 1:43 ticks
Fails 1/2: - 1:22 ticks
Fails 1/3: - 1:14 ticks
Fails 1/5: - 1:9 ticks
Fails 1/7: - 1:7 ticks
Fails 1/13: - 1:4 ticks
Fails 1/17: - 1:3 ticks
Fails 1/23: - 1:2 ticks
...
Fails 1/43: - 1:2 ticks
Fails 1/47: - 1:1 ticks

如果你期望它的失败概率小于约1/47,那么就选择异常处理。


以上假设您每次运行GetProperties()。通过为每种类型在字典或类似数据结构中缓存GetProperties()的结果,您可以加快该过程。如果您一遍又一遍地检查相同的类型集,则这可能会有所帮助。

10
如果适当的话,我很赞同并喜欢在工作中运用反射。与 Try/Catch 相比,它的优势仅在于异常被抛出时。因此,在这里使用反射之前,某人应该先问一下 - 它可能是某种方式吗?你的代码90%或甚至75%的时间会通过吗?那么 Try/Catch 仍然是最佳选择。如果情况不确定,或者有太多选择让一个成为最有可能的,那么你的反射就是正确的。 - bland
2
谢谢,现在看起来非常完整。 - bland
1
@dav_i,这两者的行为不同,所以进行比较是不公平的。svick的答案更加完整。 - nawfal
2
@dav_i 不,它们的功能不同。Martijn的答案检查C#中常规编译时类型上是否存在属性,该类型被声明为动态(这意味着它忽略了编译时安全检查)。而svick的答案检查属性是否存在于真正的动态对象上,即实现IIDynamicMetaObjectProvider的对象。我理解你回答的动机,并感激它。这样回答是公平的。 - nawfal
@dav_i - 反射检查可能会对一些行为类似于Expando(允许其属性集增长)的COM对象失败,而异常则不会。当然,这有点小众,但仍然存在。 - quetzalcoatl
显示剩余6条评论

66

或许可以使用反射?

dynamic myVar = GetDataThatLooksVerySimilarButNotTheSame();
Type typeOfDynamic = myVar.GetType();
bool exist = typeOfDynamic.GetProperties().Where(p => p.Name.Equals("PropertyName")).Any(); 

3
“我可以使用GetType(),但我更愿意避免使用它。” - roundcrisis
这个跟我的建议有相同的缺点吧?RouteValueDictionary使用反射来获取属性 - Steve Wilkes
17
可以省略“Where”关键字:.Any(p => p.Name.Equals("PropertyName")) - dav_i
请查看我的答案链接(https://dev59.com/s3A75IYBdhLWcg3w-OZA#20001358),以便比较答案。 - dav_i
3
作为一条简短的代码:((Type)myVar.GetType()).GetProperties().Any(x => x.Name.Equals("PropertyName"))。需要进行类型转换以使编译器对lambda表达式更加满意。 - MushinNoShin
你可以将此操作转化为一个函数,并在一行中进行测试,如下所示:if (PropertyExists(myObject, "PropertyName")) { var x = myObject.PropertyName; } - Alex P.

54

以防万一,如果有人需要:

如果方法 GetDataThatLooksVerySimilarButNotTheSame() 返回一个 ExpandoObject,在检查之前也可以将其转换为 IDictionary

dynamic test = new System.Dynamic.ExpandoObject();
test.foo = "bar";

if (((IDictionary<string, object>)test).ContainsKey("foo"))
{
    Console.WriteLine(test.foo);
}

4
不确定为什么这个答案没有更多的投票,因为它完全做到了要求的事情(没有异常抛出或反射)。 - Wolfshead
11
如果您知道您的动态对象是ExpandoObject或实现了IDictionary<string,object>接口的其他对象,那么@Wolfshead的答案很好,但如果它是其他类型,则会失败。 - Damian Powell

11

解决这个问题的两种常见方法是发出调用并捕获 RuntimeBinderException 异常,使用反射来检查调用,或将其序列化为文本格式并从中解析。异常的问题在于它们非常慢,因为在构造异常时,当前调用堆栈被序列化。将其序列化为 JSON 或类似的东西会产生类似的惩罚。这使我们只能使用反射,但它仅在基础对象实际上是 POCO 并且具有真正的成员时才起作用。如果它是字典的动态包装器、COM 对象或外部 Web 服务,则反射无法帮助。

另一种解决方案是使用 IDynamicMetaObjectProvider 获取 DLR 视图下的成员名称。在下面的示例中,我使用一个静态类 (Dynamic) 来测试 Age 字段并显示它。

class Program
{
    static void Main()
    {
        dynamic x = new ExpandoObject();

        x.Name = "Damian Powell";
        x.Age = "21 (probably)";

        if (Dynamic.HasMember(x, "Age"))
        {
            Console.WriteLine("Age={0}", x.Age);
        }
    }
}

public static class Dynamic
{
    public static bool HasMember(object dynObj, string memberName)
    {
        return GetMemberNames(dynObj).Contains(memberName);
    }

    public static IEnumerable<string> GetMemberNames(object dynObj)
    {
        var metaObjProvider = dynObj as IDynamicMetaObjectProvider;

        if (null == metaObjProvider) throw new InvalidOperationException(
            "The supplied object must be a dynamic object " +
            "(i.e. it must implement IDynamicMetaObjectProvider)"
        );

        var metaObj = metaObjProvider.GetMetaObject(
            Expression.Constant(metaObjProvider)
        );

        var memberNames = metaObj.GetDynamicMemberNames();

        return memberNames;
    }
}

事实证明,Dynamitey nuget包已经实现了这个功能。(https://www.nuget.org/packages/Dynamitey/) - Damian Powell
无法工作,在VS2022上进行了测试,GetMembersName返回null,解析具有许多属性的对象。 - user5583316

9

丹尼斯的回答让我想到了另一种使用JsonObjects的解决方案,

一个头部属性检查器:

Predicate<object> hasHeader = jsonObject =>
                                 ((JObject)jsonObject).OfType<JProperty>()
                                     .Any(prop => prop.Name == "header");

或者更好的说:
Predicate<object> hasHeader = jsonObject =>
                                 ((JObject)jsonObject).Property("header") != null;

例如:

举个例子:

dynamic json = JsonConvert.DeserializeObject(data);
string header = hasHeader(json) ? json.header : null;

1
请问有没有可能知道这个答案哪里出了问题? - Charles HETIER
不知道为什么这个被投票否决了,对我来说很有效。我将每个属性的谓词移动到一个帮助类中,并调用Invoke方法从每个属性中返回一个bool值。 - markp3rry
@CharlesHETIER 因为这个问题与JSON无关。 - Ian Kemp
@IanKemp 我认为您错过了代码的目的。这种特定的解决Json API问题的想法可能并不是自然的方式,但它确实提供了一种解决问题的方法。 - Charles HETIER

7

我曾经遇到过类似的问题,但是是在单元测试中。

使用SharpTestsEx,您可以检查属性是否存在。我在测试我的控制器时使用它,因为由于JSON对象是动态的,有人可能会更改名称并忘记在JavaScript或其他地方进行更改,因此在编写控制器时测试所有属性应该会提高我的安全性。

例如:

dynamic testedObject = new ExpandoObject();
testedObject.MyName = "I am a testing object";

现在,使用SharTestsEx:
Executing.This(delegate {var unused = testedObject.MyName; }).Should().NotThrow();
Executing.This(delegate {var unused = testedObject.NotExistingProperty; }).Should().Throw();

使用这种方法,我使用“Should().NotThrow()”测试所有现有属性。

这可能与主题无关,但对某些人很有用。


谢谢,非常有用。我使用SharpTestsEx来测试动态属性的值,代码如下:((string)(testedObject.MyName)).Should().Be("I am a testing object"); - Remko Jansen

1

继@karask的回答之后,您可以将该函数包装为助手,如下所示:

public static bool HasProperty(ExpandoObject expandoObj,
                               string name)
{
    return ((IDictionary<string, object>)expandoObj).ContainsKey(name);
}

1
对我来说,这个有效:

if (IsProperty(() => DynamicObject.MyProperty))
  ; // do stuff



delegate string GetValueDelegate();

private bool IsProperty(GetValueDelegate getValueMethod)
{
    try
    {
        //we're not interesting in the return value.
        //What we need to know is whether an exception occurred or not

        var v = getValueMethod();
        return v != null;
    }
    catch (RuntimeBinderException)
    {
        return false;
    }
    catch
    {
        return true;
    }
}

“null”并不意味着该属性不存在。 - quetzalcoatl
我知道,但如果它是空的,我不需要对值进行任何操作,因此对于我的用例来说,这是可以的。 - Jester

0
如果您的用例是将 API 响应转换为仅关注少数字段,可以使用以下代码:
var template = new { address = new { street = "" } };
var response = JsonConvert.DeserializeAnonymousType(await result.Content.ReadAsStringAsync(), template);

string street = response?.address?.street;

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