Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
206 views
in Technique[技术] by (71.8m points)

c# - Action delegates, generics, covariance and contravariance

I have two business contract classes:

public BusinessContract

public Person : BusinessContract

In another class I have the following code:

private Action<BusinessContract> _foo;

public void Foo<T>( Action<T> bar ) where T : BusinessContract
{
    _foo = bar;
}

The above won't even compile, which baffles me a bit. I'm constraining T to be BusinessContract, so why doesn't the compiler know that bar can be assigned to _foo?

In trying to get around this, we tried changing it to the following:

public void Foo<T>( Action<T> bar ) where T : BusinessContract
{
    _foo = (Action<BusinessContract>)bar;
}

Now the compiler is happy, so I write the following code elsewhere in my application:

Foo<Person>( p => p.Name = "Joe" );

And the app blows up with an InvalidCastException at run-time.

I don't get it. Shouldn't I be able to cast my more specific type to a less specific type and assign it?

UPDATE

Jon answered the question so got the nod for that, but just to close the loop on this, here's how we ended up solving the problem.

private Action<BusinessContract> _foo;

public void Foo<T>( Action<T> bar ) where T : BusinessContract
{
    _foo = contract => bar( (T)contract );
}

Why are we doing this? We have a Fake DAL we use for unit testing. With one of the methods we need to give the test developer the ability to specify what the method should do when it's called during the test (it's a refresh method that updates a cached object from the database). The purpose of Foo is to set what should happen when refresh is called. IOW, elsewhere in this class we have the following.

public void Refresh( BusinessContract contract )
{
    if( _foo != null )
    {
        _foo( contract );
    }
}

The test developer could then, for example, decide they wanted to set the name to a different value when Refresh was called.

Foo<Person>( p => p.Name = "New Name" );
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

You've got the covariance and contravariance the wrong way round. Let's consider Action<object> and Action<string>. Removing the actual generics, you're trying to do something like this:

private Action<object> _foo;

public void Foo(Action<string> bar)
{
    // This won't compile...
    _foo = bar;
}

Now suppose we then write:

_foo(new Button());

That's fine, because Action<object> can be passed any object... but we've initialized it with a delegate which must take a string argument. Ouch.

This isn't type safe, so doesn't compile.

The other way would work though:

private Action<string> _foo;

public void Foo(Action<object> bar)
{
    // This is fine...
    _foo = bar;
}

Now when we invoke _foo, we have to pass in a string - but that's fine, because we've initialized it with a delegate which can take any object reference as a parameter, so it's fine that we happen to be giving it a string.

So basically Action<T> is contravariant - whereas Func<T> is covariant:

Func<string> bar = ...;
Func<object> foo = bar; // This is fine
object x = foo(); // This is guaranteed to be okay

It's not clear what you're trying to do with the action, so unfortunately I can't really give any advice on how to get around this...


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...