Type safety
Do you think this is a good idea or do you see anything that could be improved?
Language preference is obviously subjective. Many languages can achieve the same goal, albeit in a different way (usually). Every language has it's pro's and cons. Type safety is a big pro for C#.
Type safety comes with its own drawbacks. A type safe compiler is considerably more pedantic, as it needs to ascertain types at all times.
"Is it a good idea?" is a bit broad. It requires us to outweigh the benefit of having type safety versus the benefit of not having type safety; which inherently requires us to evaluate how important type safety is, and how needed your current fake duck typing solution is.
Secondly, this can devolve into a discussion as to how complex you want your future development to be, and how risky you want your runtime to be. These are all subjective boundaries; not every developer will see eye to eye here.
This is a source of endless discussion. An experienced Javascript developer will be used to having no type safety, and maybe won't think that type safety is all that necessary. A C# developer might instead strongly rely on type safety and will feel in uncharted territory when they abandon type safety, and therefore refuse to work without it.
To prevent this discussion, I'm going to draw a reasonable line of compromise here, for the sake of providing an answer which is not endlessly argumentative.
You're using C#, so I assume you want to leverage the benefits of C#. Type safety is desirable. When two solutions exist to the same problem, and only one of them is type safe; you must pick the type safe option.
Effectively, solutions without type safety are only valid in absence of type safe solutions.
Relying on compiler inference.
In a type safe language, relations need to be defined explicitly. That is the purpose of type safety: it only allows things it knows are correct. Without mentioning that Foo.Print()
and Bar.Print()
have something in common; the compiler will never guess that they have something in common just because they have the same signature.
For example:
public class NumberMultiplier
{
public int Execute(int a, int b) { return a * b; }
}
public class UserStateUpdater
{
public void Execute(int userId, int stateId) { /* set User state in DB */ }
}
Should these classes be abstracted in a way that you can reusably call Execute
on either class, since they have the same signature? I don't see a reason to.
Based on your expectation that the compiler sees the similarity between Foo.Print()
and Bar.Print()
, that means that the compiler will then also draw a similar parallel between NumberMultiplier.Execute()
and UserStateUpdater.Execute()
.
This is leading to a system where you would define architectural structure via naming; which is wrong for many reasons. It inherently causes naming conflicts (avoiding particular names can lower readability as you'd have to invent apt synonyms which is not always feasible) and violates encapsulation (since the developer of NumberMultiplier
and the developer of UserStateUpdater
need to coordinate in order to prevent the compiler from making unwanted inferences.
Taking this further; imagine if either of these classes is part of a library that is used. The consumer of said library would effectively be disallowed from using a method with a similar signature, regardlesss of which namespace or class he's working it.
Imagine if both methods are part of two separate libraries that are being used. What then?
Relations need to be defined explicitly
Your expectation (and workaround to enable that expectation) feels very dirty. Ignoring static
for a second; if you have two classes that have the same property/method and you wish to point out the relation between Foo.Print()
and Bar.Print()
, you need to express that relation, usually through inheritance or an interface.
You're correct that neither solution can be (directly) utilized for static
methods and properties. However, that is a technical limitation, not an explicit philosophical decision.
The inability to declare a relationship is not an implicit statement by the language developers that you shouldn't express that relation. You're inferring that this is the case based on the absence of a possibility, which is essentially trying to prove a negative ("There is no car in my garage. Therefore, cars should not be stored in garages").
The issue here is that it's a clash of intentions:
- Statics exist to provide global information that is supposed to be universally correct.
- Having a value that is different for different classes inherently means that the value is not universally correct, since it's different on different levels.
The second bullet point effectively negates the need for a static.
I think you are confusing a static property and a reusable/related type.
The use case
public class Foo
{
public static string Baz => "Baaz-1";
public static void Print(string message) => Console.WriteLine(message ?? Baz);
}
public class Bar
{
public static string Baz => "Baaz-2";
public static void Print(string message) => Console.WriteLine(message ?? Baz);
}
I genuinely can't think of a use case where this is both relevant and the better approach. Your second (real) example isn't helping me clarify this. I understand the code, but not why statics are the better approach here.
If you're expecting to resuably refer to static properties of types; that suggests that you already want to rely on a system where types are used dynamically (the same way values are), which is simply not what C# is built for (Delphi, on the other hand, has taken this approach to heart). While that doesn't mean it's impossible to implement it in C#, you're orthogonally going against the grain of C#. This creates a complication of the code base and a lot more work for developers; which you need to justify by showcasing what you stand to gain by doing so. I can't see a valid justification for this in your posted code or explanation.
Regardless, that can be a failure to understand the use case on my part. Let's assume that the use case for statics is valid.
A clearer alternative
You can implement this by separating the static property from the expressed type relation; by creating an additional (class) abstraction.
public class MyStaticThing
{
public string Baz;
public void Print(string message) => Console.WriteLine(message ?? Baz);
}
public class Foo
{
public static MyStaticThing Static = new MyStaticThing()
{
Baz = "Baaz-1"
};
}
public class Bar
{
public static MyStaticThing Static = new MyStaticThing()
{
Baz = "Baaz-2"
};
}
Foo
still has its static property. Every Foo
objects has the same MyStaticThing
.
Foo.Static
has different values from Bar.Static
, but they both have the same properties/methods (enforced by type safety).
- Implementing the
MyStaticThing
class has prevented you from violating DRY (your Print()
methods were a clear copy/paste job).
- You can handle both static property values in a reusable manner:
public void HandleMyStaticThing(MyStaticThing myThing)
{
//....
}
Is your answer actually solving a problem?
I've been looking at it again and again, and I simply can't see the purpose of your duck typing. It seems to complicate the syntax without providing anything in return.
I noticed that you're not really handling your type in a reusable pattern. Every usage example explicitly lists the type you're expecting:
DuckObject.Quack(typeof(Foo), duck => duck.Baz).Dump();
DuckObject.Quack(typeof(Bar), duck => duck.Baz).Dump();
DuckObject<Foo>.Quack(duck => duck.Baz).Dump();
DuckObject<Bar>.Quack(duck => duck.Baz).Dump();
DuckObject<Bar>.Quack(duck => duck.Print("test"));
DuckObject<Bar>.Quack(duck => duck.Print(null));
DuckObject<Bar>.Quack(duck => duck["Bum!"]); // throws
This effectively negates what you're trying to do. Without implementing any change to your code, every line can already be rewritten to not use your extra implementation layer, while retaining the same functionality.
DuckObject.Quack<string>(typeof(Foo), duck => duck.Baz)
(and similar) can be rewritten as Foo.Baz
(and similar).
DuckObject<Foo>.Quack<string>(duck => duck.Baz)
(and similar) can be rewritten as Foo.Baz
(and similar).
There is one interesting difference:
DuckObject<Bar>.Quack<string>(duck => duck["Bum!"]);
This throws a runtime exception.
Bar["Bum!"];
This throws a (quite obvious) compile time exception. Furthermore, this also has nothing to do with the static properties of Bar
, so I'm not sure why this usage is relevant in the current decision.
Having compile time exceptions is better than having runtime exceptions. A developer isn't required to run the application in order to find out if his code is syntactically valid.
Applying the stance I posted in the beginning, this means that the second option is preferred, as it retains type safety.
Additionally:
DuckObject<Bar>.Quack<string>(duck => duck.Baz);
DuckObject<Bar>.Quack<string>(duck => duck["Bum!"]);
Your intended usage imples that you think static and non-static properties should be addressed the same way.
This is simply not the case!
Bar myBar = new Bar() { Name = "Test" };
var nonStaticValue = myBar.Name;
var staticValue = Bar.Baz;
Notice the difference between myBar.Name
and Bar.Baz
. One references the object, the other references the type. This is a fundamental difference.
The way you're trying to enforce that static and non-static properties are retrieved the same way, suggests to me that you misunderstand statics from a structural point of view. That leads me to wonder if you're actually using statics appropriately in your classes; which in turn raises questions as to whether your fake duck typing solution is based on nothing more than a design flaw.
This isn't meant as harsh as it possibly sounds over written text; but the more I consider your intentions, the more I'm getting convinced that your fake duck typing is the Y of the XY problem where X (using statics this way) is the real issue.
Func<dynamic, dynamic>
. \$\endgroup\$ – Denis Apr 14 '18 at 14:43dynamic
, this is the trick. \$\endgroup\$ – t3chb0t Apr 14 '18 at 14:45DuckObject
doesn't have aBuz
property, does it?duck => duck.Baz
, duck isDuckObject
here. Or am I missing something? \$\endgroup\$ – Denis Apr 14 '18 at 14:46DynamicObject
and the runtime calls the respecitveoverride
and lets you pick that property/field/method - here by reflection fromT
. \$\endgroup\$ – t3chb0t Apr 14 '18 at 14:47TryGetMember
. \$\endgroup\$ – Denis Apr 14 '18 at 15:10