views:

190

answers:

2

Hi, I have some example data:

public struct Task
{
    public int INT;
    public string STRING;
    public DateTime? NULLABLEDATETIME;
}

And function, which uses it:

public KeyValuePair<Expression<Func<Task, object>>, object> Marry(Expression<Func<Task, object>> key, object value)
{
    return new KeyValuePair<Expression<Func<Task, object>>, object>(key, value);
}

Here is example of function call:

Marry(t => t.INT, 1984);
Marry(t => t.NULLABLEDATETIME, DateTime.Now);
Marry(t => t.STRING, "SomeSting");

This code works, no questions. Unfortunatly we can also call functions like below, because String and int are both inherited from object class:

Marry(t => t.INT, "SomeSting");

I want to say compiler, that first and second parameters have same data type: int -> int, string -> string, DateTime? -> DateTime? and check it during compilation. I tried this:

public KeyValuePair<Expression<Func<Task, T1>>, T2> Marry<T1, T2>(Expression<Func<Task, T1>> key, T2 value)
    where T2 : T1
{
    return new KeyValuePair<Expression<Func<Task, T1>>, T2>(key, value);
}

This almost works, and if I try to put wrong data like this Marry(t => t.INT, "SomeSting"); the compiler reports an error: The type 'string' cannot be used as type parameter 'T2' in the generic type or method 'Task.Marry(System.Linq.Expressions.Expression>, T2)'. There is no implicit reference conversion from 'string' to 'int'.

But this solution does not work with null. When I call Marry(t => t.NULLABLEDATETIME, null); the compiler says: The type arguments for method 'Marry(System.Linq.Expressions.Expression>, T2)' cannot be inferred from the usage. Try specifying the type arguments explicitly.

Why? I already know the data type: DateTime?. I don't want to explicitly call Marry<DateTime?>(t => t.NULLABLEDATETIME, null);. How can I do this - or is there another way to check some function parameter types during compilation?

+3  A: 

Why don't you simply replace T1 and T2 with a single generic type parameter? Then the second parameters type needn't be inferred - which it doesn't have to be anyhow since it must be identical to that of the first parameter.

That would look like this:

Variant 1

public KeyValuePair<Expression<Func<Task, T>>, T> 
    Marry<T>(
       Expression<Func<Task, T>> key, 
       T value) {
    return new KeyValuePair<Expression<Func<Task, T>>, T>(key, value);
}

I don't recommend this, but if you want the type-inference to "greedily" determine the type of the expression based solely on the first parameter (so that you get the error "cannot convert from 'string' to 'DateTime?'), you can curry the method:

Variant 2

public static Func<T, KeyValuePair<Expression<Func<Task, T>>, T> >
    Marry<T>(
        Expression<Func<Task, T>> key) {
    return (value=>new KeyValuePair<Expression<Func<Task, T>>, T>(key, value));
}
//usage:
Marry(t => t.NULLABLEDATETIME)("test"); 
//error: Delegate 'System.Func<System.DateTime?,System.Collections.Generic.KeyValuePair<System.Linq.Expressions.Expression<System.Func<UserQuery.Task,System.DateTime?>>,System.DateTime?>>' has some invalid arguments
//  - Argument 1: cannot convert from 'string' to 'System.DateTime?'

That error message essentially covers it. However, the API isn't too readable and the usage of heavily nested generic types makes things... less readable. A longer variant with essentially the same behavior but shorter error messages could look as follows:

Variant 3

public class MarriagePartner<T> {
    readonly Expression<Func<Task, T>> key;
    public MarriagePartner(Expression<Func<Task, T>> key) { this.key = key; }
    public  KeyValuePair<Expression<Func<Task, T>>, T> With(T value) {
        return new KeyValuePair<Expression<Func<Task, T>>, T>(key, value);
    }
}

public static MarriagePartner<T> Marry2<T>(Expression<Func<Task, T>> key) { 
    return new MarriagePartner<T>(key);
}

//Usage:
Marry2(t => t.NULLABLEDATETIME).With("test"); 
//error: The best overloaded method match for 'UserQuery.MarriagePartner<System.DateTime?>.With(System.DateTime?)' has some invalid arguments
//  - Argument 1: cannot convert from 'string' to 'System.DateTime?'

That error message is a little more specific. I think Variant 1's error message suffices and it's what I'd use (KISS and all) - but if you Really want that casting error message, Variant 3 has the most understandable (if longer) implementation and most friendly error message.

Eamon Nerbonne
Because now when call function with wrong parameters Marry(t => t.NULLABLEDATETIME, "SomeString"); compiler says:The type arguments for method 'Lambda.Db.Task.Task.Marry<T>(System.Linq.Expressions.Expression<System.Func<Task,T>>, T)' cannot be inferred from the usage. Try specifying the type arguments explicitly.As you can see, we still have this message. I would like compiler to say:There is no implicit reference conversion from 'string' to 'DateTime?'.
Oleg Mazko
It's not as serious an error, however. With the T1+T2 method, correct cases (informally: T1 string, T2 null) can't be resolved, whereas with this method, although the error message may not be ideal, more correct cases work and incorrect cases don't.
Eamon Nerbonne
Also, the error message you're seeing is standard for linq. For instance, try: `Enumerable.Concat(new[]{3},new[]{"test"});` which results in the same error message.
Eamon Nerbonne
If you really want the error message to give you an "casting" error, I've added two variants which do that.
Eamon Nerbonne
+1  A: 

You can cast null to some type

(string)null, (object)null
Gopher