如何处理Foo <t>的集合,其中每个项目的T可以不同?</t>

时间:2012-12-14 12:43:26

标签: c# generics collections type-systems

问题描述

我正在尝试存储一系列通用Foo<T>元素,其中T可能因每个项目而异。我还有DoSomething<T>(Foo<T>)这样的函数可以接受任何Foo<T>的{​​{1}}。看起来我应该能够在上面列表的每个元素上调用这个函数,因为它们都是函数的有效参数,但我似乎无法向C#编译器表达这个想法。

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

Foo<?>

此模式在Java中有效,但我如何在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); } } 类型),但我真的很想看看是否有是一个更简单的解决方案,不会放弃静态类型安全。

如下所述,仅使用dynamic或非泛型超类型,不允许我调用需要object的函数。但是,即使我对Foo<T>一无所知,这也是明智的。例如,我可以使用T从某处检索Foo<T>,从其他地方检索List<T> list,然后调用T value,编译器将知道所有类型正确。

动机

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

list.Add(value)

除了Value属性之外,组件当然还有许多其他成员(例如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; } } 个事件)。

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

IUiComponent<T>

我们可以通过在所有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>()来撤消所有内容:

IUiComponent

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

示例代码

这是一小段代码,您可以使用它来开发/检查解决方案。任务是将几个public void Undo(List<IUiComponent<?>> components) { Undo(m_TextBox); Undo(m_DatePicker); // ... } - 对象放入某种集合对象中,然后调用接受此集合对象的函数并交换每个Pair<T>First字段。{{1} (使用Second)。理想情况下,您不应使用任何演员表或反射。如果您可以设法在不修改Pair<T> - 类的情况下以任何方式执行此操作,则可获得奖励积分:)

Application.Swap()

4 个答案:

答案 0 :(得分: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是类型安全的,因为它只能包含Pair s(好吧,IPair s)。唯一的问题是价值类型。正如您所看到的,我必须将数组的最后一个元素声明为Pair<object>而不是Pair<int>。 这是因为covariance/contravariance don't work with value types所以我必须在int中添加object

======

编辑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包裹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中因类型擦除而丢失的完整类型信息。

答案 1 :(得分: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在接口和具体类型中具有泛型约束,则元素可以传递给对其参数类型具有这些约束的方法(否则,即使使用Reflection,这也是非常困难的)。添加可以接受传递参数的接口和调用方法的版本也可能很有用:

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

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

答案 2 :(得分: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#显然不允许泛型委托/ lambdas,你不能指定内联访问者并利用闭包 - 据我所知,每个访问者需要是一个新的显式定义的类,可能有额外的参数作为字段。 Foo类型还必须通过实现访问者模式明确支持此方案。

答案 3 :(得分:0)

对于那些仍然觉得有趣的人来说,这是我能想出的最佳解决方案,也符合&#34;奖励要求&#34;不以任何方式接触原始类型。它基本上是一种访问者模式,我们不会将Foo<T>直接存储在我们的容器中,而是存储一个在IFooVisitor上调用Foo<T>的委托。请注意我们如何轻松地列出这些列表,因为T实际上并不是代表的一部分&#39;类型。

// 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)