Does C# pick the wrong type for var when parsing a

2020-02-10 04:23发布

问题:

I am using the following code to convert some Json into a dynamic object. When I use DateTime.Parse on a property of my dynamic type I would expect the var to guess that it's type is a DateTime... instead, it stays as a dynamic. This can't be right, can it?

Full example below.

var settings = new JavaScriptSerializer().Deserialize<dynamic>(json);

var startDate = DateTime.Parse(settings.startDate);
var endDate = DateTime.Parse(settings.endDate);
var userId = int.Parse(settings.userId);

startDate, endDate and userId are all still dynamic, which means I then cannot use them in a later Lambda expressions. Obviously, I can fix the code with:

DateTime startDate = DateTime.Parse(settings.startDate);
DateTime endDate = DateTime.Parse(settings.endDate);
int userId = int.Parse(settings.userId);

..but it seems like the compiler is making a 'bad guess'. Can anyone explain this to me?

Thanks

回答1:

..but it seems like the compiler is making a 'bad guess'. Can anyone explain this to me?

When you use dynamic, the entire expression is treated at compile time as a dynamic expression, which causes the compiler to treat everything as dynamic and get run-time binding.

This is explained in 7.2 of the C# Language specification:

When no dynamic expressions are involved, C# defaults to static binding, which means that the compile-time types of constituent expressions are used in the selection process. However, when one of the constituent expressions in the operations listed above is a dynamic expression, the operation is instead dynamically bound.

This basically means that most operations (the types are listed in section 7.2 of the spec) which have any element that is declared as dynamic will be evaluated as dynamic, and the result will be a dynamic.

In your case, this statement:

var settings = new JavaScriptSerializer().Deserialize<dynamic>(json);

Uses dynamic, so, it getst reated as a dynamic expression. Since "Method invocation" is one of the C# operations subject to binding (7.2), the compiler treats this as dynamic bound, which causes this to evaluate to:

dynamic settings = new JavaScriptSerializer().Deserialize<dynamic>(json);

This, in turn, causes the DateTime.Parse expressions to be dynamic bound, which in turn makes them return dynamic.

Your "fix" works when you do DateTime startDate = DateTime.Parse(settings.startDate); because this forces an implicit dynamic conversion (described in section 6.1.8 of the spec) of the result of the DateTime.Parse method to a DateTime:

An implicit dynamic conversion exists from an expression of type dynamic to any type T. The conversion is dynamically bound (§7.2.2), which means that an implicit conversion will be sought at run-time from the run-time type of the expression to T. If no conversion is found, a run-time exception is thrown.

In this case, the conversion is valid, so you effectively switch everything back to static binding from then on.



回答2:

I don't think this is particularly surprising.

DateTime.Parse(<dynamic>) will evaluate to dynamic.

DateTime startDate = <dynamic> does a runtime assignment from a dynamic to a DateTime.

You've just combined the two.

The compiler isn't guessing the type of DateTime.Parse(<dynamic>) to be anything other than dynamic, but it's clever enough to realise that if you do an assignment of this value to a DateTime then assuming it is successful you're left with a DateTime.



回答3:

This is per the spec. See §7.6.5:

An invocation-expression is dynamically bound (§7.2.2) if at least one of the following holds:

• The primary-expression has compile-time type dynamic.

• At least one argument of the optional argument-list has compile-time type dynamic and the primary-expression does not have a delegate type.

Consider this scenario:

class Foo {
    public int M(string s) { return 0; }
    public string M(int s) { return String.Empty; }
}

Foo foo = new Foo();
dynamic d = // something dynamic
var m = foo.M(d);

What should be the compile-time type of m? The compiler can't tell, because it won't know until runtime which overload of Foo.M is invoked. Thus, it says m is dynamic.

Now, you could say that it should be able to figure out DateTime.Parse has only one overload, and even if it didn't but all of its overloads have the same return type that in that case it should be able to figure out the compile-time type. That would be a fair point, and is probably best Eric Lippert. I asked a separate question to get insight into this: Why does a method invocation expression have type dynamic even when there is only one possible return type?.



回答4:

There are two different concepts here

  1. dynamic: which is any type that is resolved at run-time
  2. var: which is implicit static typing that is performed at compile time

So if you do

var settings = new JavaScriptSerializer().Deserialize<dynamic>(json);
var startDate = DateTime.Parse(settings.startDate);

at compile time it is resolved to dynamic type and at run time it is resolved to a concrete type. Compiler checks the right part new JavaScriptSerializer().Deserialize<dynamic>(json);, which returns dynamic. At compile time this instructs compiler to drop all type-safty checks and live it till run time.

This code

var settings = new JavaScriptSerializer().Deserialize<dynamic>(json);
DateTime startDate = DateTime.Parse(settings.startDate);

explicitly says that the dynamic object is of concrete type, therefore compiler is able to infer the type, all its methods and is able to perform static type check at compile time.