A "dynamic" object cannot see interface members with only default implementations.This is something I came across when exploring properties and default implementation. But it's interesting enough to warrant a closer look.
The code for this article is available on GitHub: jeremybytes/interfaces-in-csharp-8, specifically the DynamicAndDefaultImplementation project.
Note: this article uses C# 8 features, which are currently only implemented in .NET Core 3.0. For these samples, I used Visual Studio 16.3.1 and .NET Core 3.0.100.
An Interface with Default Implementation
To show how dynamic works (or doesn't work), we'll use an interface with a default implementation for a method. The example here is an interface that represents a regular polygon -- that is, a geometric shape with 3 or more sides where each side is the same length.
Here is the interface (from the IRegularPolygon.cs file):
The "GetPerimeter" method has a default implementation and uses the expression-bodied member syntax. Since the perimeter of a shape is calculated the same regardless of the number of sides, we can provide a default implementation in the interface. (Whether this is a good approach or not is going to be discussed in a future article.)
The Implementation
This interface is implemented by the Square class (from the Square.cs file):
As shown here, the Square class provides implementations for the NumberOfSides and SideLength properties from the interface, and it has an implementation of the GetArea method from the interface (this also uses the expression-bodied member syntax).
Notice that the GetPerimeter method is *not* implemented. The class instead relies on the default implementation provided by the interface.
Using the Implementation
This project is a console application, and the Main method creates two Square objects and displays them (from the Program.cs file -- the final file has some error handling that isn't shown here):
Showing Members with the Interface
Here is the "ShowInteracePolygon" method (also from the Program.cs file):
This method takes our interface as a parameter. Then it displays the properties and methods that are part of the interface. This method call works fine with both of our Square objects.
The first call is made with "smallSquare". Since "smallSquare" uses "var", the variable is a Square type. (For more information on "var", take a look at Demystifying the "var" Keyword in C#.) But because the parameter is "IRegularPolygon", the "smallSquare" gets cast to the interface for use in the method.
The second call is made with "largeSquare". This variable is explicitly typed as "IRegularPolygon", so there is no casting necessary when the method is called.
A Problem with Dynamic
The problem comes in with the "ShowDynamicPolygon" method. I have used "dynamic" in the past so that a method can work with various types that all have the same properties and methods even if the types are incompatible.
As a side note, I also like to use "dynamic" when I need to pick one or two values out of a deserialized JSON object. This saves me from having to create a separate class or do mapping. There are risks of runtime errors, but sometimes it is worth the risk.
Here is the code for ShowDynamicPolygon (also in the Program.cs file):
The body of the method is identical to "ShowInterfacePolygon". The only difference is that the parameter is typed as "dynamic".
Unfortunately, this fails.
Failure with the Concrete Type (Square)
The first call to this method (using "smallSquare") fails:
The exception is a RuntimeBinderException. This is thrown when the Runtime Binder cannot find the requested member on the object. In this case, the exception tells us that "GetPerimeter" is not part of the "Square" class.
And that is absolutely correct. If we look back at the Square class, we see that it does not have a "GetPerimeter" method:
So this failure makes sense.
Failure with the Interface (IRegularPolygon)
The second call to the method (using "largeSquare") also fails:
This is a bit more difficult to understand. The "largeSquare" variable is typed to our interface, IRegularPolygon, and the interface *does* have the "GetPerimeter" method.
But that is not how "dynamic" works here. If we look at the exception, we see that we have the same message that we saw earlier: "GetPerimeter" is not part of "Square".
The Runtime Binder is not looking at the type of the object, it is looking at the object itself. Specifically, it is looking at the visible members of the object. Since the object is a "Square", that is what the Runtime Binder sees.
Bug or Intended?
I'm not sure if this is a bug or if this is how it is intended to work. In looking through the design proposal, I did not see any references to "dynamic". So this may be an oversight.
Or it might be intentional. "dynamic" and the DLR (dynamic language runtime) haven't gotten much love lately. The issues in the GitHub repository for .NET are a bit complicated (and there are a ton of them), so I haven't taken the time to research whether this shows up as an issue in the repository.
Some Error Handling
To avoid runtime exceptions, the final code has some try/catch blocks (in the Program.cs file):
This catches the RuntimeBinderException and shows the message on the console. Here's what that output looks like:
So, we can see that "dynamic" cannot see the default implementation of an interface member.
But then if we keep going, things get a bit more interesting.
Inconsistent Behavior
Understanding how "dynamic" behaves with default implementation is good on the surface, but things get strange if we have some classes that use the default implementation and some classes that override the default.
Here's a Triangle class (from the Triangle.cs file):
Triangle also implements the IRegularPolygon interface. But notice that it provides its own implementation for the "GetPerimeter" method. This overrides the default behavior in the interface itself.
We can create an display the triangle (in the Program.cs file):
And everything works!
This shows that both the "ShowInterfacePolygon" and "ShowDynamicPolygon" methods work with the Triangle class.
The Difference
The difference is that Triangle has its own GetPerimeter method, so the Runtime Binder finds it when using the "dynamic" parameter.
Now this seems a bit dangerous. The "dynamic" method works sometimes but not others.
If a class relies on a default implementation, it fails:
And if a class provides its own implementation, it succeeds.
This is a little bit scary.
Wrap Up
So we need to be a bit careful when using "dynamic" around interfaces that have default implementations for members. If a default implementation is used, then "dynamic" will fail to find it. If the class provides its own implementation, "dynamic" finds it just fine.
The changes in C# 8 make interfaces more complicated than I would like them to be. But digging into them and exploring is how we figure out what works and what doesn't work.
After we have a good handle on the mechanics, we can take a closer look at how to use the new features in a safe and understandable way. But that will have to wait for a future article.
Happy Coding!
No comments:
Post a Comment