1

问题描述

我正在尝试存储通用Foo<T>元素的集合,T每个项目的位置可能不同。我也有类似DoSomething<T>(Foo<T>)的功能可以接受Foo<T>任何T. 似乎我应该能够在上述列表的每个元素上调用这个函数,因为它们都是函数的有效参数,但我似乎无法向 C# 编译器表达这个想法。

据我所知,问题在于我不能真正表达这样的列表,因为 C# 不允许我在Foo<T>没有绑定的情况下编写T。我想要的是类似于 Java 的通配符机制 ( Foo<?>)。以下是它在 Pseudo-C# 中的外观,其中存在此​​通配符类型:

class Foo<T> {
    // ...
}

static class Functions {
    public static void DoSomething<T>(Foo<T> foo) {
        // ...
    }

    public static void DoSomething(List<Foo<?>> list) {
        foreach(Foo<?> item in list)
            DoSomething(item);
    }
}

这种模式在 Java 中是有效的,但我如何在 C# 中做同样的事情?我已经尝试了一些解决方案,我将在下面的答案中发布,但我觉得应该有更好的方法。

注意:我已经“足够好”地解决了这个问题以满足我的实际需求,并且我知道解决它的方法(例如使用dynamic类型),但我真的很想看看是否有一个更简单的解决方案不会放弃静态类型安全。

正如下面所建议的,仅使用objector 非泛型超类型不允许我调用需要Foo<T>. 但是,即使我对T. 例如,我可以使用来从某处Foo<T>检索 a ,从其他地方检索 a ,然后调用,编译器将知道所有类型都正确。List<T> listT valuelist.Add(value)

动机

有人问我为什么我需要这样的东西,所以我正在编一个更接近大多数开发人员日常经验的例子。想象一下,您正在编写一堆 UI 组件,这些组件允许用户操作某种类型的值:

public interface IUiComponent<T> {
    T Value { get; set; }
}

public class TextBox : IUiComponent<string> {
    public string Value { get; set; }
}

public class DatePicker : IUiComponent<DateTime> {
    public DateTime Value { get; set; }
}

除了 Value 属性,组件当然还有许多其他成员(例如OnChange事件)。

现在让我们添加一个撤消系统。我们不应该为此修改 UI 元素本身,因为我们已经可以访问所有相关数据——只需连接OnChange事件,并且每当用户更改 UI 组件时,我们都会存储每个的值IUiComponent<T>(有点浪费,但让我们保持简单)。为了存储值,我们将在表单中使用Stack<T>for each IUiComponent<T>。使用IUiComponent<T>as 键可以访问这些列表。我将省略列表存储方式的详细信息(如果您认为这很重要,我将提供一个实现)。

public class UndoEnabledForm {
    public Stack<T> GetUndoStack<T>(IUiComponent<T> component) {
        // Implementation left as an exercise to the reader :P
    }

    // Undo for ONE element. Note that this works and is typesafe,
    // even though we don't know anything about T...
    private void Undo<T>(IUiComponent<T> component) {
        component.Value = GetHistory(component).Pop();
    }
    
    // ...but how do we implement undoing ALL components?
    // Using Pseudo-C# once more:
    public void Undo(List<IUiComponent<?>> components) {
        foreach(IUiComponent<?> component in components)
            Undo(component);
    }
}

我们可以通过直接调用Undo<T>()所有IUiComponents(按名称)来撤消所有操作:

public void Undo(List<IUiComponent<?>> components) {
    Undo(m_TextBox);
    Undo(m_DatePicker);
    // ...
}

但是,我想避免这种情况,因为这意味着如果您添加/删除一个组件,您将不得不在代码中多接触一个地方。如果您想要对所有组件执行数十个字段和更多功能(例如,将它们的所有值写入数据库并再次检索它们),这可能会产生大量重复。

示例代码

这是一小段代码,可用于开发/检查解决方案。任务是将多个Pair<T>- 对象放入某种集合对象中,然后调用一个函数,该函数接受该集合对象并交换每个对象的FirstandSecond字段Pair<T>(使用Application.Swap())。理想情况下,您不应使用任何强制转换或反射。如果您可以在不以任何方式修改 -class 的情况下设法做到这一点,则可以加分Pair<T>:)

class Pair<T> {
    public T First, Second;

    public override string ToString() {
        return String.Format("({0},{1})", First, Second);
    }    
}

static class Application {
    static void Swap<T>(Pair<T> pair) {
        T temp = pair.First;
        pair.First = pair.Second;
        pair.Second = temp;
    }

    static void Main() {
        Pair<int> pair1 = new Pair<int> { First = 1, Second = 2 };
        Pair<string> pair2 = new Pair<string> { First = "first", Second = "second" };
        // imagine more pairs here

        // Silly solution
        Swap(pair1);
        Swap(pair2);

        // Check result
        Console.WriteLine(pair1);
        Console.WriteLine(pair2);
        Console.ReadLine();
    }
}
4

4 回答 4

1

编辑2:就您的大修问题而言,该方法与我之前向您提出的方法基本相同。在这里,我正在根据您的场景对其进行调整,并更好地评论是什么使它起作用(加上一个不幸的“陷阱”与值类型......)

// note how IPair<T> is covariant with T (the "out" keyword)
public interface IPair<out T> {
     T First {get;}
     T Second {get;}
}

// I get no bonus points... I've had to touch Pair to add the interface
// note that you can't make classes covariant or contravariant, so I 
// could not just declare Pair<out T> but had to do it through the interface
public class Pair<T> : IPair<T> {
    public T First {get; set;}
    public T Second {get; set;}

    // overriding ToString is not strictly needed... 
    // it's just to "prettify" the output of Console.WriteLine
    public override string ToString() {
        return String.Format("({0},{1})", First, Second); 
    }    
}

public static class Application {
    // Swap now works with IPairs, but is fully generic, type safe
    // and contains no casts      
    public static IPair<T> Swap<T>(IPair<T> pair) {
        return new Pair<T>{First=pair.Second, Second=pair.First};       
    }

    // as IPair is immutable, it can only swapped in place by 
    // creating a new one and assigning it to a ref
    public static void SwapInPlace<T>(ref IPair<T> pair) {
        pair = new Pair<T>{First=pair.Second, Second=pair.First};
    }

    // now SwapAll works, but only with Array, not with List 
    // (my understanding is that while the Array's indexer returns
    // a reference to the actual element, List's indexer only returns
    // a copy of its value, so it can't be switched in place
    public static void SwapAll(IPair<object>[] pairs) {
        for(int i=0; i < pairs.Length; i++) {
           SwapInPlace(ref pairs[i]);
        }
    }
}

这或多或少是......现在main你可以这样做:

var pairs = new IPair<object>[] {
    new Pair<string>{First="a", Second="b"},
    new Pair<Uri> {
               First=new Uri("http://www.site1.com"), 
               Second=new Uri("http://www.site2.com")},     
    new Pair<object>{First=1, Second=2}     
};

Application.SwapAll(pairs);
foreach(var p in pairs) Console.WriteLine(p.ToString());

输出

(b,a)
(http://www.site2.com/,http://www.site1.com/)
(2,1)

因此,您的 Array 是类型安全的,因为它只能包含Pairs(嗯,IPairs)。唯一的问题是值类型。如您所见,我必须将数组的最后一个元素声明为 aPair<object>而不是Pair<int>我希望的那样。这是因为协变/逆变不适用于值类型,所以我必须intobject.

=========

编辑1(旧的,只是留下作为参考以理解下面的评论):当您需要对容器采取行动时,您可以同时拥有一个非通用标记界面(但不关心“包装”类型) 和一个协变通用的,用于当您需要类型信息时。

就像是:

interface IFoo {}
interface IFoo<out T> : IFoo {
    T Value {get;}
}

class Foo<T> : IFoo<T> {
    readonly T _value;
    public Foo(T value) {this._value=value;}
    public T Value {get {return _value;}}
}

假设你有这个简单的类层次结构:

public class Person 
{
    public virtual string Name {get {return "anonymous";}}
}

public class Paolo : Person 
{
    public override string Name {get {return "Paolo";}}
}

您可以拥有可以在任何IFoo(当您不在乎是否Foo包装 a时Person)或专门在IFoo<Person>(当您在乎时)起作用的功能:例如

static class Functions 
{
    // this is where you would do DoSomethingWithContainer(IFoo<?> foo)
    // with hypothetical java-like wildcards 
    public static void DoSomethingWithContainer(IFoo foo) 
    {
        Console.WriteLine(foo.GetType().ToString());
    }

    public static void DoSomethingWithGenericContainer<T>(IFoo<T> el) 
    {
        Console.WriteLine(el.Value.GetType().ToString());
    }

    public static void DoSomethingWithContent(IFoo<Person> el) 
    {
        Console.WriteLine(el.Value.Name);
    }

}

你可以这样使用:

    // note that IFoo can be covariant, but Foo can't,
    // so we need a List<IFoo  
    var lst = new List<IFoo<Person>>
    {   
        new Foo<Person>(new Person()),
        new Foo<Paolo>(new Paolo())
    };


    foreach(var p in lst) Functions.DoSomethingWithContainer(p);    
    foreach(var p in lst) Functions.DoSomethingWithGenericContainer<Person>(p);
    foreach(var p in lst) Functions.DoSomethingWithContent(p);
// OUTPUT (LinqPad)
// UserQuery+Foo`1[UserQuery+Person]
// UserQuery+Foo`1[UserQuery+Paolo]
// UserQuery+Person
// UserQuery+Paolo
// anonymous
// Paolo

输出中值得注意的一件事是,即使只接收 IFoo 的函数仍然具有并打印了在 java 中会因类型擦除而丢失的完整类型信息。

于 2012-12-14T13:00:48.260 回答
1

我建议你定义一个接口来调用你想要调用的函数DoSomething<T>(T param)。最简单的形式:

public interface IDoSomething
  { void DoSomething<T>(T param); }

接下来定义一个基本类型ElementThatCanDoSomething

abstract public class ElementThatCanDoSomething
  { abstract public void DoIt(IDoSomething action); }

和一个通用的具体类型:

public class ElementThatCanDoSomething><T>
{
  T data;
  ElementThatCanDoSomething(T dat) { data = dat; }

  override public void DoIt(IDoSomething action)
    { action.DoIt<T>(data); }
}

现在可以为任何类型的编译时 T 构造一个元素,并将该元素传递给一个泛型方法,保持类型T(即使该元素为 null,或者该元素是 的派生元素T)。上面的具体实现并不是非常有用,但它可以很容易地以许多有用的方式进行扩展。例如,如果类型T在接口和具体类型中具有通用约束,则可以将元素传递给对其参数类型具有这些约束的方法(否则这非常困难,即使使用反射也是如此)。添加可以接受传递参数的接口和调用程序方法的版本也可能很有用:

public interface IDoSomething<TX1>
{ void DoSomething<T>(T param, ref TX1 xparam1); }

... and within the ElementThatCanToSomething

  abstract public void DoIt<TX1>(IDoSomething<TX1> action, ref TX1 xparam1);

... and within the ElementThatCanToSomething<T>

  override public void DoIt<TX1>(IDoSomething<TX1> action, ref TX1 xparam1)
    { action.DoIt<T>(data, ref xparam1); }

该模式可以很容易地扩展到任意数量的传递参数。

于 2012-12-14T23:03:10.943 回答
0

似乎在 C# 中,您必须创建一个 的列表Foo,将其用作Foo<T>. 但是,您不能轻易地Foo<T>从那里返回。

Foo我发现的一个解决方案是为每个函数添加一个抽象方法,并通过调用SomeFn<T>(Foo<T>)来实现它们。但是,这意味着每次您想在 上定义一个新的(外部)函数时,您都必须向 中添加一个转发函数,即使它确实不必知道该函数:Foo<T>SomeFn(this)Foo<T>Foo

abstract class Foo {
    public abstract void DoSomething();
}

class Foo<T> : Foo {
    public override void DoSomething() {
        Functions.DoSomething(this);
    }
    // ...
}

static class Functions {
    public static void DoSomething<T>(Foo<T> foo) {
        // ...
    }

    public static void DoSomething(List<Foo> list) {
        foreach(Foo item in list)
            item.DoSomething();
    }
}

从设计的角度来看,一个稍微干净的解决方案似乎是访问者模式,它在一定程度上概括了上述方法,并切断了Foo与特定泛型函数之间的耦合,但这使得整个事情变得更加冗长和复杂。

interface IFooVisitor {
    void Visit<T>(Foo<T> foo);
}

class DoSomethingFooVisitor : IFooVisitor {
    public void Visit<T>(Foo<T> foo) {
        // ...
    }
}

abstract class Foo {
    public abstract void Accept(IFooVisitor foo);
}

class Foo<T> : Foo {
    public override void Accept(IFooVisitor foo) {
        foo.Visit(this);
    }
    // ...
}

static class Functions {
    public static void DoSomething(List<Foo> list) {
        IFooVisitor visitor = new DoSomethingFooVisitor();
        foreach (Foo item in list)
            item.Accept(visitor);
    }
}

如果创建访问者更容易,这几乎是一个很好的解决方案 IMO。由于 C# 显然不允许泛型委托/lambda,因此您不能指定访问者内联并利用闭包 - 据我所知,每个访问者都需要是一个新的明确定义的类,并可能有额外的参数作为字段。该Foo类型还必须通过实现访问者模式来明确支持此方案。

于 2012-12-14T12:43:56.030 回答
0

对于那些可能仍然觉得这很有趣的人,这是我能想出的最好的解决方案,它也满足不以任何方式接触原始类型的“奖励要求”。它基本上是一个访问者模式,我们不Foo<T>直接将 存储在我们的容器中,而是存储一个IFooVisitor在我们的Foo<T>. 注意我们是如何轻松列出这些的,因为T它实际上并不是代表类型的一部分。

// The original type, unmodified
class Pair<T> {
    public T First, Second;
}

// Interface for any Action on a Pair<T>
interface IPairVisitor {
    void Visit<T>(Pair<T> pair);
}

class PairSwapVisitor : IPairVisitor {
    public void Visit<T>(Pair<T> pair) {
        Application.Swap(pair);
    }
}

class PairPrintVisitor : IPairVisitor {
    public void Visit<T>(Pair<T> pair) {
        Console.WriteLine("Pair<{0}>: ({1},{2})", typeof(T), pair.First, pair.Second);
    }
}

// General interface for a container that follows the Visitor pattern
interface IVisitableContainer<T> {
    void Accept(T visitor);
}

// The implementation of our Pair-Container
class VisitablePairList : IVisitableContainer<IPairVisitor> {
    private List<Action<IPairVisitor>> m_visitables = new List<Action<IPairVisitor>>();

    public void Add<T>(Pair<T> pair) {
        m_visitables.Add(visitor => visitor.Visit(pair));
    }

    public void Accept(IPairVisitor visitor) {
        foreach (Action<IPairVisitor> visitable in m_visitables)
            visitable(visitor);
    }
}

static class Application {
    public static void Swap<T>(Pair<T> pair) {
        T temp = pair.First;
        pair.First = pair.Second;
        pair.Second = temp;
    }

    static void Main() {
        VisitablePairList list = new VisitablePairList();
        list.Add(new Pair<int> { First = 1, Second = 2 });
        list.Add(new Pair<string> { First = "first", Second = "second" });

        list.Accept(new PairSwapVisitor());
        list.Accept(new PairPrintVisitor());
        Console.ReadLine();
    }
}

输出:

Pair<System.Int32>: (2,1)
Pair<System.String>: (second,first)
于 2012-12-18T17:32:07.527 回答