【问题标题】:How to handle a collection of Foo<T>, where T can be different for each item?如何处理 Foo<T> 的集合,其中每个项目的 T 可能不同?
【发布时间】:2012-12-19 01:38:19
【问题描述】:

问题描述

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

据我所知,问题在于我不能真正表达这样的列表,因为 C# 不允许我在没有绑定 T 的情况下编写 Foo&lt;T&gt;。我想要的是类似于 Java 的通配符机制 (Foo&lt;?&gt;)。以下是它在 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 类型),但我真的很想看看是否有不放弃静态类型安全的更简单的解决方案。

仅使用object 或非泛型超类型(如下所示)不允许我调用需要Foo&lt;T&gt; 的函数。但是,即使我对T 一无所知,这也是明智的。例如,我可以使用Foo&lt;T&gt; 从某处检索List&lt;T&gt; list,从其他某处检索T value,然后调用list.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&lt;T&gt;(有点浪费,但让我们保持简单)。为了存储值,我们将为表单中的每个IUiComponent&lt;T&gt; 使用Stack&lt;T&gt;。这些列表通过使用IUiComponent&lt;T&gt; 作为键来访问。我将省略列表存储方式的详细信息(如果您认为这很重要,我将提供一个实现)。

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

我们可以通过在所有IUiComponents(按名称)上直接调用Undo&lt;T&gt;() 来撤消所有操作:

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

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

示例代码

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

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

【问题讨论】:

  • 不使用泛型怎么样?看起来你想要一个对象的集合。
  • 也许您可以详细说明为什么要将Foo 设为泛型? Foo&lt;?&gt; 本质上是动态的,那么为什么要使用泛型呢?

标签: c# generics collections type-systems


【解决方案1】:

我建议你定义一个接口来调用你想要调用的函数DoSomething&lt;T&gt;(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,或者该元素是 @ 的派生元素) 987654327@)。上面的具体实现并不是非常有用,但它可以很容易地以许多有用的方式进行扩展。例如,如果类型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); }

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

【讨论】:

  • +1 因为这是一个有效的解决方案,但请注意它与我自己的访客解决方案完全相同(只是名称更有趣)。不过,关于约束的有趣想法。至于传递参数,我可能更喜欢将它们存储在 IDoSomething 实现中。
  • @Medo42:如果在实现IDoSomething 的对象中传递参数,那么可能需要为每种此类类型创建一个不同的实例,每次调用时;如果对象不需要任何字段,则可以简单地创建一个单例实例并无限期地使用它。此外,使用参数允许一段代码可以例如将IDoSomething&lt;int&gt; 传递给项目集合,该集合可以在每个项目上调用它,为第一项传递0,为第二项传递1,等等。如果IDoSomething 不...
  • ...取一个参数。顺便说一句,另一个可能不是好事的技巧是向IDoSomething 添加一个虚拟类型参数,并将其用作方法选择器。如果该方法与参数传递相结合,则将接口嵌套在泛型类中可能会有所帮助,例如SelectedActor&lt;TDummy&gt;.IDoSomething&lt;XP1,XP2&gt; 以帮助明确哪些类型是真实的,哪些是虚拟的。我不知道我应该深入了解接口可以实现的扩展可能性(协变和逆变增加了更多乐趣)。
【解决方案2】:

编辑 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)。唯一的问题是值类型。如您所见,我必须将数组的最后一个元素声明为Pair&lt;object&gt;,而不是我希望的Pair&lt;int&gt;。 这是因为covariance/contravariance don't work with value types 所以我不得不在object 中输入int

=========

EDIT 1(旧的,只是留在那里作为参考以理解下面的 cmets): 当您需要对容器进行操作时(但不关心“包装”类型),您可以同时拥有一个非泛型标记接口,以及在需要类型信息时使用协变泛型。

类似:

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&lt;Person&gt;(当您关心时)的函数: 例如

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

【讨论】:

  • 问题是这不允许我调用需要Foo&lt;T&gt; 的函数——即使我对T 一无所知,这样的事情也可能是明智的。例如,我可以使用Foo&lt;T&gt; 从某处检索List&lt;T&gt; list,从其他位置检索T value,然后调用list.Add(value),编译器将知道所有类型都正确。
  • 更接近,但仍然没有雪茄 - DoSomethingWithGenericContainer 几乎是我想要的,但请注意,您必须在调用时将 T 显式绑定为 Person。在此函数中添加Console.WriteLine(typeof(T)); 可确认T 始终为Person。这在我上面的列表示例中不好,因为如果我的实际泛型类型是Foo&lt;Paolo&gt;,则列表将是List&lt;Paolo&gt;,并且尝试将Person 添加到该列表中不能静态成功。我认为问题需要的是一个实际的代码示例,它既可以作为动机,也可以验证建议的解决方案。我稍后会添加一个。
  • @Medo42 是的,一个实际的例子会有所帮助......请注意,在 c# 中,有时您在调用泛型方法时不需要指定类型,而只是因为编译器可以推断它。该方法在运行时仍然知道实际类型是什么,因为泛型在 .net 中被具体化了。
  • 我彻底检查了问题,现在在底部包含一个简单的示例问题。
  • @Medo42 ...我已经根据您提出的方案调整了我的答案
【解决方案3】:

似乎在 C# 中,您必须创建一个 Foo 列表,将其用作 Foo&lt;T&gt; 的基本类型。但是,您不能轻易地从那里回到Foo&lt;T&gt;

我找到的一个解决方案是在Foo 中为每个函数SomeFn&lt;T&gt;(Foo&lt;T&gt;) 添加一个抽象方法,并通过调用SomeFn(this)Foo&lt;T&gt; 中实现它们。但是,这意味着每次您想在Foo&lt;T&gt; 上定义一个新的(外部)函数时,您都必须向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 类型还必须通过实现访问者模式来明确支持此方案。

【讨论】:

    【解决方案4】:

    对于那些可能仍然觉得这很有趣的人,这是我能想到的最好的解决方案,它也满足不以任何方式接触原始类型的“奖励要求”。它基本上是一个访客模式,我们不直接将Foo&lt;T&gt; 存储在我们的容器中,而是在我们的Foo&lt;T&gt; 上存储一个调用IFooVisitor 的委托。请注意我们是如何轻松列出这些的,因为 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)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多