如何正确实现通用接口

时间:2015-09-01 10:06:16

标签: c# generics interface

我有点问题。 下面是一个代码示例。如何使用testVar这样的变量可以ITest<T>?显然我得到了一个施法错误。有没有什么方法可以完全使用通用接口而不进行转换? 我尝试T协变(out T),但由于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();
        }
    }
}

3 个答案:

答案 0 :(得分:3)

没有简单的方法可以让它发挥作用。

你的变量testVar承诺我得到一个具有事件arg的事件的类,我可以将Result设置为任何对象

您想要分配的是一个类,最后我只能string分配给Result属性。对于能够设置任何对象的上述承诺,这不起作用。

如果T的所有部分都是只读的,您可以解决此问题,因为读取对象可以读取字符串。但写作是有问题的。

答案 1 :(得分:1)

看看这里:

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

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

您不能拥有ITest<object>,因为stringobject的规范。您必须将ITest设置为string

编辑: 您还可以使TestClass通用:

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>();
    }
}

答案 2 :(得分:1)

没有

C#泛型介于Java的泛型和C ++模板之间。在Java中,这很容易 - 您只需使用ITest<?>。在C#和C ++中,这是不可能的。

虽然Java的泛型仅在编译时存在(基本上,为了允许额外的静态类型检查),C#是真实的&#34;在运行时也是如此。例如,想象一个简单的泛型类,除了另一个值的包装之外什么都不是:

class Test<T>
{
  public T Value;
}

在Java中,该字段的类型始终为Object - Test<?>Test<String>Test<Integer>在运行时完全相同。

在C#中,该字段将完全泛型类型参数中指定的类型。这是巨大的差异 - 例如,这意味着如果您使用值类型,则不会将其装箱。这也意味着Test<?>没有任何意义 - 泛型类型的不同实例可以具有完全不同的内存布局。这也意味着只允许通常的方差:子类型可以分配给父类型,反之则不然 - 可以传递子类型而不是父类型,反之则不然。

这解释了您使界面协变的麻烦 - 简单地说,它不是协变。 EventHandler 传递您的值而不是返回它,因此您只能传递子类型,而不是父类型(例如,传递string而不是object object,但不是string而不是ITest<string>)。唯一可能的差异是逆变 - 您可以使用ITest<object>来存储Func<T>的实例。 Action<T>是协变的,而Func<T, U>是逆变的。当然,T相对于UITest<Whatever>的协变而言是逆变的。

如何解决实际问题完全取决于您的问题 。例如,如果您只构建对象集合,则可以使用非通用接口来公开您需要的任何内容。您也可以手动将值转换为foreach (var component in Components.OfType<ITest<Whatever>>()) ... - 尽管这显然意味着在某种程度上失去了类型安全性。或者,如果您只需要在任何时候使用特定的实例化,您可以单独使用每种类型:

ShowDialog

修改

在你的情况下,似乎你想根据一些不是模态的对话框的结果做一些动作,否则你只需使用ShowDialogAsync并同步完成,对吧?

当然,这需要某种形式的异步性。最简单的是一个简单的委托 - 让我们假设你在每个单独的对话框上都有一个方法public static void ShowDialogAsync(Action<string> action) { ... } ,接受一个函数,该函数在确认时接受对话框提供的参数。例如,返回单个字符串的对话框可以使用如下方法:

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

这里的关键是对话框知道你提供的确切代表类型,调用者也可以这样做 - 不需要存储&#34;全局&#34;在任何地方。

电话会看起来像这样:

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();
}

因此,每当对话框完成时,将使用从对话框的文本框中获取的参数调用强类型委托(例如)。整个对话框看起来像这样:

Task

如果您之前见过awaitShowDialogAsync,那么此处的模式应该是显而易见的 - 实际上,让Task<string>返回Action<string>是微不足道的而不是接受var username = await UsernameDialog.ShowDialogAsync(); ,允许你这样做:

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

事实上,

Task<T>

这两种方法非常对称 - 主要区别在于思维; Action<T>&#34;拉&#34;结果,而ContinueWith&#34;推动&#34;结果:虽然很明显,任何一方都可以用来做相反的事情;在Task<T>示例中,我已使用Action<T> 推送值。从string result = null; UsernameDialog.ShowDialogAsync(i => { result = i; }); 中提取值也非常简单:

Action<T>

虽然应该注意,只有ShowDialogAsyncTask<T>退出之前实际执行时才有意义 - Task<T>.Result通过在您使用{{1}时等待结果来处理此问题};你可以通过使用类似的东西对动作做同样的事情:

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

waitHandle.Wait();

显然,这是一个人为的例子 - 使用异步回调只是为了强制它们是同步的 - 但它应该说明对称性。在现实世界中,您只需返回Task<T>,或使用异步回调。

这四个示例中的关键点是,从不需要任何全局状态来处理对话 - 无论是同步还是异步处理它。这使您可以在实际需要时始终使用已知类型 - 当您显示对话框时,它是已知对话框,并且参数已知;当你处理回调(或承诺)时,你再次在实际知道它意味着什么的代码片段中这样做。

我们假设您需要显示一个对话框,允许您更改另一种形式的用户名 - 即使使用手动回调,这也很简单

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

这里的诀窍是使用闭包 - 基本上,lblUsername(更准确地说,this,即表格)是默默地&#34;传递&#34;以及回调,而不必是Action委托的明确参数!

这是完全一般的,并且允许您使用函数编写任何您想要的东西(严肃地说,我们在这里谈论数学定理 - 任何命令式程序都可以转换为功能性程序 - 代表可以更多(或者少于)功能)。

我坚持回归Task<...>如果它有意义 - 回调很好,但很容易在回调地狱中结束。 <{1}}与Task<...>一起使用很多更容易。