Reputation: 133
I am attempting to add compile time checks to code which previously took the string name of a data object's property and a value of type 'object'. I am doing this in order to ensure the property and value are actually of the same type to prevent cases where they are not from introducing a runtime bug.
I am handling the compile time check by creating a method which takes an expression of type Expression<Func<TDataObject, TPropertyValue>>
and a parameter of type TPropertyValue
. From this, I can inspect the expression to get the name of the property that would be returned and then use the same logic we have today with strings and value as object type.
public interface IPropertyDictionary<TDataObject> where TDataObject : class
{
void AddIfSameType<TProperty>(
Expression<Func<TDataObject, TProperty>> propertyAccessExpression,
TProperty propertyValue);
}
The following works as expected:
// Allowed
propDictionary.AddIfSameType(e => e.IntProperty, 123);
// Flagged by intellisense (though the expression is flagged rather than second parameter...)
propDictionary.AddIfSameType(e => e.IntProperty, "asdf");
However, this does not work as expected:
// Not flagged as error
propDictionary.AddIfSameType(e => e.IntProperty, 123L);
With that, C# infers TPropertyValue to be a long rather than an int. In the debugger, I can see that the expression is being transformed to cast it to a long:
e => Convert(e.IntProperty)
In my ideal case, C# would prefer the type of IntProperty when making it's type inference and raise a compile time error indicating casting from long to int requires an explicit cast. Is there are way to indicate to C# that it should only use the first parameter of the method when inferring the type? The only alternative I have at the moment is to explicitly provide the type parameter:
// Flagged by intellisense
propDictionary.AddIfSameType<int>(e => e.IntProperty, 123L);
But in the 99% case people will not pass a type parameter and I would not expect them to realize they need to do so in this situation. As a result, their bug again becomes one of the runtime errors that I am eager to avoid.
Upvotes: 3
Views: 1091
Reputation: 112362
How is C# supposed to know that TDataObject
has a property IntProperty
? All you say is that TDataObject
must be a class
with where TDataObject : class
. You must specify a constraint that lets C# know about this property (and possibly others). E.g.
public interface IProperties
{
int IntProperty { get; set; }
double DoubleProperty { get; set; }
string StringProperty { get; set; }
}
public interface IPropertyDictionary<TDataObject> where TDataObject : IProperties
{
void AddIfSameType<TProperty>(
Expression<Func<TDataObject, TProperty>> getProp, TProperty value);
}
Then you can declare a dictionary with
public class PropDictionary<TDataObject> : IPropertyDictionary<TDataObject>
where TDataObject : IProperties
{
public void AddIfSameType<TProperty>(
Expression<Func<TDataObject, TProperty>> getProp, TProperty value)
{
}
}
and a data class
public class DataObject : IProperties
{
public int IntProperty { get; set; }
public double DoubleProperty { get; set; }
public string StringProperty { get; set; }
}
Now both of these calls work
var propDictionary = new PropDictionary<DataObject>();
propDictionary.AddIfSameType(e => e.DoubleProperty, 123);
propDictionary.AddIfSameType(e => e.IntProperty, 123L);
Why do they work?
The first one because the types are inferred to be
void ProperDictionary<DataObject>.AddIfSameType<double>(
Expression<Func<DataObject, double>> getProp, double value)
The int
value passed is simply cast to double
.
The second case is a bit surprising. The types are inefrred to be
void ProperDictionary<DataObject>.AddIfSameType<long>(
Expression<Func<DataObject, long>> getProp, longvalue)
I assume that the return value is implicitly widened:
e => (long)e.IntProperty
Conclusion: C# more clever than you might think. It infers the generic types and automatically casts values to make it work where possible.
UPDATE
Therefore use two distinct type parameters for the Func
return type and the value.
public void AddIfSameType<TProp, TValue>(
Expression<Func<TDataObject, TProp>> getProp, TValue value)
{
if (typeof(TProp) == typeof(TValue)) {
} else {
}
}
C# will infer the exact type for each of these type parameters. You could also type the value as object
instead, but with the cost of boxing of value types.
Upvotes: 0
Reputation: 156524
There's not a way to do what you're describing while preserving the exact syntax you're asking for. (I stand corrected: see David Browne's answer for a way to do it.)
If you're willing to change your approach, you could separate the method call with the expression from the one with the value. Something like this:
propDictionary.AdderFor(e => e.IntProperty).Add(123L);
One potential benefit of this approach is that you could capture the "adder" as a variable.
var intAdder = propDictionary.AdderFor(e => e.IntProperty)
intAdder.Add(456); // okay
intAdder.Add(123L); // error
Upvotes: 4
Reputation: 89090
Yes, in a slightly roundabout way. Use two type parameters, and a type parameter constraint. There isn't a equality constraint, but the inheritance constraint should work for most scenarios:
static void AddIfSameType<TLProp,TRProp>(Func<DataObject,TLProp> lprop, TRProp rprop) where TRProp : TLProp
{
}
static void Main(string[] args)
{
AddIfSameType(d => d.IntProperty, 1);
//compiles
AddIfSameType(d => d.IntProperty, 1L);
//Error CS0315 The type 'long' cannot be used as type parameter 'TRProp'
//... There is no boxing conversion from 'long' to 'int'.
}
Upvotes: 6