我正在尝试存储一系列通用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()
答案 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)