Eric
Eric

Reputation: 133

Is there a way to hint to C# how to perform generic type inference?

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

Answers (3)

Olivier Jacot-Descombes
Olivier Jacot-Descombes

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

StriplingWarrior
StriplingWarrior

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

David Browne - Microsoft
David Browne - Microsoft

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

Related Questions