简体   繁体   中英

How to correctly implement generic interfaces

I have a little problem. Below is a code sample. How can I have a variable like testVar that can take ITest<T> ? Obviously I get an cast error. Is there some way I can fully use the generic interface without casting? I tried to make T covariant (out T) , but that's not possible due to the ResultEventArgs .

using System;

namespace InterfaceGenericFunApp
{
    public class ResultEventArgs<T> : EventArgs
    {
        public T Result { get; set; }
    }

    public interface ITest<T>
    {
        event EventHandler<ResultEventArgs<T>> ResultReceived;
    }

    public class TestClass : ITest<string>
    {
        public event EventHandler<ResultEventArgs<string>> ResultReceived;
    }

    public class Program
    {
        private static ITest<object> testVar; 

        public static void Main(string[] args)
        {
            testVar = new TestClass();
        }
    }
}

There is no simple way to make it work.

Your variable testVar promises that I get a class that has an event that has an event arg where I can set the Result to any object .

What you want to assign is a class where in the end I can only assign a string to the Result property. That does not work with the promise above to be able to set any object.

You may be able to work this out if all parts of T are read-only, because reading an object can be reading a string. But writing is problematic.

Take a look here:

public class Program
{
    private static ITest<string> testVar;

    public static void Main(string[] args)
    {
        testVar = new TestClass();
    }
}

You can't have ITest<object> , because a string is a specification of object . You have to set ITest to string .

EDIT: You could also make TestClass generic:

public class TestClass<T> : ITest<T>
{
    public event EventHandler<ResultEventArgs<T>> ResultReceived;
}

public class Program
{
    private static ITest<string> testVar;

    public static void Main(string[] args)
    {
        testVar = new TestClass<string>();
    }
}

No.

C# generics are somewhere between Java's generics and C++'s templates. In Java, this would be easy - you'd just use ITest<?> . In C# and C++, this is impossible.

While Java's generics only really exist at compile-time (basically, to allow additional static type checks), C#'s are "real" in the runtime as well. For example, imagine a simple generic class that's nothing but a wrapper over another value:

class Test<T>
{
  public T Value;
}

In Java, the field will always be of type Object - Test<?> , Test<String> and Test<Integer> are exactly the same type in runtime.

In C#, the field will be exactly the type specified in the generic type argument. This is a huge difference - for example, it means that if you use a value-type, it isn't going to be boxed. It also means that Test<?> doesn't make any sense - different instantiations of the generic type can have entirely different memory layouts. This also means that only the usual variance is allowed: a child type can be assigned to a parent type, but not vice versa - a child type can be passed instead of a parent type, but not vice versa.

This explains your trouble with making the interface covariant - simply put, it isn't covariant. EventHandler is passing your value rather than returning it, so you can only ever pass child types, not parent types (eg it's fine to pass string instead of object , but not object instead of string ). The only variance possible is contravariance - you could use ITest<string> to store an instance of ITest<object> . Func<T> is covariant, while Action<T> is contravariant. And of course, Func<T, U> is contravariant with respect to T and covariant with respect to U .

How you can solve your actual problems depends entirely on what your problem actually is . For example, if you're only building a collection of objects, you could use a non-generic interface that exposes whatever you need. You can also manually cast the value to a ITest<Whatever> - though that obviously means losing type safety to an extent. Or, if you only need to work with specific instantiations at any time, you can work with each type separately like so:

foreach (var component in Components.OfType<ITest<Whatever>>()) ...

EDIT:

In your case, it seems that you want to do some action based on the result of some dialog which isn't modal, otherwise you'd simply use ShowDialog and complete synchronously, right?

This calls for some form of asynchronicity, of course. The simplest being a simple delegate - let's suppose you have a method ShowDialogAsync on each of your individual dialogs, taking a function which in turn takes an argument supplied by the dialog when it is confirmed. For example, a dialog that returns a single string could have a method like this:

public static void ShowDialogAsync(Action<string> action) { ... }

The key here is that the dialog knows the exact delegate type you supply, and the caller does as well - no need to store a "global" field anywhere.

The call could look something like this:

UsernameDialog.ShowDialogAsync(i => Console.WriteLine(i));

So whenever the dialog completes, the strongly typed delegate is invoked with the argument taken from the dialog's textbox (for example). The entire dialog could look something like this:

private readonly Action<string> _action;

private UsernameDialog(Action<string> action)
{
  _action = action;
}

public static void ShowDialogAsync(Action<string> action) 
{
  var dialog = new UsernameDialog(action);
  dialog.Show();
}

// I wouldn't actually bind this to a button click, this is just an example
public void btnOk_Click(object sender, EventArgs e)
{
  _action(tbxUsername.Text);
  Close();
}

If you've ever seen Task or await before, the pattern here should be obvious - indeed, it is trivial to make ShowDialogAsync return a Task<string> instead of accepting Action<string> , allowing you to do this:

var username = await UsernameDialog.ShowDialogAsync();

And indeed,

UsernameDialog.ShowDialogAsync().ContinueWith(t => Console.WriteLine(t.Result));

The two approaches are quite symmetric - the main difference is in the thinking; Task<T> "pulls" the result, while Action<T> "pushes" the result: though it's obvious that either can be used to do the opposite as well; in the ContinueWith sample, I've used Task<T> to push a value. Pulling a value from Action<T> is also quite easy:

string result = null;
UsernameDialog.ShowDialogAsync(i => { result = i; });

Though it should be noted that this only makes sense if the Action<T> is actually executed before ShowDialogAsync exits - Task<T> handles this by waiting for the result when you use Task<T>.Result ; you could do the same with the action by using something like this:

string result = null;
var waitHandle = new ManualResetEventSlim();
UsernameDialog.ShowDialogAsync(i => { result = i; waitHandle.Set(); });

waitHandle.Wait();

Obviously, this is a contrived example - there's little point in using asynchronous callbacks just to force them to be synchronous - but it should illustrate the symmetry. In the real world, you'd simply return a Task<T> , or use the asynchronous callback.

The key point in all four of those examples is that you never needed any global state to handle the dialog - whether you treat it synchronously or asynchronously. This allows you to always work with known types whenever you actually need to - when you show a dialog, it's a known dialog, and the arguments are known; when you handle the callback (or promise), you again do so in the piece of code that actually knows what it means.

Let's say that you need to show a dialog that allows you to change the username in another form - even using the manual callback, this is as easy as

UsernameDialog.ShowDialogAsync(userName => { lblUsername.Text = username; });

The trick here is the use of a closure - basically, lblUsername (to be more exact, this , ie, the form) was silently "passed" along with the callback, without having to be an explicit argument of the Action delegate!

This is entirely general, and allows you to program anything you want using functions (seriously, we're talking about math theorems here - any imperative program can be converted into a functional one - and delegates are little more (or less) than functions).

I'd stick to returning Task<...> if it makes sense - callbacks are fine, but it's quite easy to end up in a callback hell. Using Task<...> along with await is much easier.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM