为什么没有ICloneable <T>?


222

是否存在ICloneable<T>不存在泛型的特定原因?

如果我不需要每次克隆某些东西时都将其强制转换,它将更加舒适。


6
没有!出于对“原因”的所有应有的尊重,我同意您的意见,他们应该予以实施!
Shimmy Weitzhandler,2010年

对于Microsoft定义,这将是一件好事(brew-your-own自己的接口的问题是,两个不同程序集中的接口即使在语义上是相同的,也将不兼容)。在设计接口时,它将具有三个成员Clone,Self和CloneIfMutable,所有这些成员将返回T(最后一个成员将视情况返回Clone或Self)。Self成员将使得可以接受ICloneable(Foo)作为参数,然后将其用作Foo,而无需进行类型转换。
supercat

这将允许适当的克隆类层次结构,其中可继承类公开了受保护的“克隆”方法,并具有公开了公有方法的密封派生类。例如,可能具有PILE,CloneablePile:Pile,EnhancedPile:Pile和CloneableEnhancedPile:EnhancedPile,如果克隆它们都不会被破坏(即使不是全部都公开了一种公共克隆方法),而进一步EnhancedPile:EnhancedPile(会被破坏) (如果已克隆,但不公开任何克隆方法)。接受ICloneable(桩)的例程可以接受CloneablePile或CloneableEnhancedPile ...
supercat

...即使CloneableEnhancedPile不继承自CloneablePile。请注意,如果EnhancedPile从CloneablePile继承,则ExtraEnhancedPile将必须公开一个公共克隆方法,并且可能会传递给希望克隆该方法的代码,这违反了Liskov替代性原则。由于CloneableEnhancedPile将实现ICloneable(EnhancedPile),并且通过隐含ICloneable(OfPile),因此可以将其传递给一个预期可复制PILE的衍生物的例程。
supercat

Answers:


111

ICloneable现在被认为是错误的API,因为它没有指定结果是深拷贝还是浅拷贝。我认为这就是为什么他们不改进此界面的原因。

您可能可以执行类型化克隆扩展方法,但我认为它将需要使用其他名称,因为扩展方法的优先级低于原始方法。


8
我不同意(好坏),它在某些时候对于SHALLOW克隆非常有用,在那些情况下,确实需要Clone <T>来节省不必要的装箱和拆箱操作。
Shimmy Weitzhandler,2010年

2
@AndreyShchekin:关于浅克隆还是浅克隆尚不清楚?如果List<T>使用克隆方法,我希望它会产生一个List<T>与原始列表中的项目具有相同标识的,但是我希望可以根据需要复制任何内部数据结构,以确保对一个列表所做的任何操作都不会影响存储在另一个中的项目的标识。歧义在哪里?克隆的一个更大的问题是“钻石问题”的一种变体:如果CloneableFoo继承自[not publicly可克隆] Foo,则应CloneableDerivedFoo继承自...
supercat 2012年

1
@supercat:我不能说我完全理解您的期望,例如您会考虑identity列表本身(例如列表的情况)?但是,忽略这一点,您的期望并不是人们在调用或实现时可能唯一的想法Clone。如果实现其他列表的图书馆作者没有遵循您的期望怎么办?该API应该是毫不含糊的,而不是毫无疑问的。
Andrey Shchekin

2
@supercat所以您正在谈论浅表复制。但是,深层复制从根本上没有错,例如,如果您要对内存中的对象执行可逆事务。您的期望基于您的用例,其他人可能会有不同的期望。如果他们将您的对象放入对象中(反之亦然),结果将是意外的。
2012年

5
@supercat您无需考虑它是.NET框架的一部分,而不是您的应用程序的一部分。因此,如果您创建一个不进行深度克隆的类,那么其他人将创建一个进行深度克隆并调用Clone其所有部分的类,则该类将无法正常工作-取决于该部分是由您还是由那个人实现的喜欢深克隆的人。您关于模式的观点是有效的,但是在API中使用IMHO还不够清楚-应该调用它ShallowCopy来强调这一点,或者根本不提供它。
Andrey Shchekin,2012年

141

除了安德烈的答复(我同意,+1) -当ICloneable 做了,你也可以选择明确的实施,使公众Clone()返回一个类型的对象:

public Foo Clone() { /* your code */ }
object ICloneable.Clone() {return Clone();}

当然,泛型还有第二个问题ICloneable<T>-继承。

如果我有:

public class Foo {}
public class Bar : Foo {}

我实施了ICloneable<T>,那我实施了ICloneable<Foo>吗?ICloneable<Bar>?您很快就开始实现许多相同的接口...与强制转换相比...真的那么糟糕吗?


10
+1我一直认为协方差问题是真正的原因
Tamas Czinege 09年

这样做有一个问题:显式接口实现必须是private,这会导致问题,应该在某些时候将Foo强制转换为ICloneable ...我在这里缺少什么吗?
乔尔(Joel)在09年

3
我的错误:事实证明,尽管 Foo 定义为私有,但如果 Foo强制转换为IClonable ,则将调用该方法(CLR通过C#2nd Ed,第319页)。似乎是一个奇怪的设计决定,但确实存在。所以Foo.Clone()提供了第一种方法,(((ICloneable)Foo).Clone()提供了第二种方法。
09年

实际上,严格来说,显式接口实现是公共API的一部分。因为它们可以通过强制公开调用。
Marc Gravell

8
最初提出的解决方案似乎很棒……直到您意识到在类层次结构中,您希望“ public Foo Clone()”是虚拟的并在派生类中被覆盖,以便它们可以返回自己的类型(并克隆自己的课程)。可悲的是,您不能在覆盖中更改返回类型(提到的协方差问题)。因此,您又重新回到了原来的问题上-明确的实现并不能真正给您带来很多好处(除了返回层次结构的基本类型而不是“对象”之外),并且您将返回大多数Clone()调用结果。!
肯·贝克特

18

我要问的是,除了实现该接口之外,您还要对接口做什么?接口通常仅在您强制转换时才有用(即此类是否支持“ IBar”),或者具有接受该参数的参数或设置器(即我采用“ IBar”)。使用ICloneable-我们遍历了整个框架,但未能在除实现之外的任何地方找到一种用法。我们也没有找到“现实世界”中任何除了实现它之外还有其他用途的用法(在我们可以访问的约60,000个应用程序中)。

现在,如果您只想强制实施一种模式,以实现您的“可克隆”对象,那就完全可以了-继续。您还可以确定“克隆”到底对您意味着什么(即深浅)。但是,在那种情况下,我们(BCL)不需要定义它。我们仅在需要在不相关的库之间交换作为抽象类型输入的实例时,才在BCL中定义抽象。

大卫·基恩(BCL团队)


1
非泛型ICloneable并不是很有用,但是ICloneable<out T>如果ISelf<out T>使用单个Selftype 方法从它继承而来,则可能会非常有用T。一个人通常不需要“可克隆的东西”,但是很可能需要一个T可克隆的东西。如果实现了可克隆对象ISelf<itsOwnType>,则需要克隆的a例程T可以接受type的参数ICloneable<T>,即使并非所有可克隆派生类T都具有相同的祖先。
2012年

谢谢。这与我上面提到的强制转换情况类似(即“此类支持'IBar')。遗憾的是,我们只提出了非常有限且孤立的场景,在这些场景中,您实际上会利用T是可克隆的这一事实。你有什么想法吗?
大卫·基恩

.net中有时很尴尬的一件事是,当一个集合的内容被认为是一个值时,就管理可变的集合(即,我以后想知道集合中现在的项目集)。这样做ICloneable<T>可能是有用的,尽管维护并行可变和不变类的更广泛的框架可能会更有用。换句话说,需要查看某种类型的Foo包含但既不打算对其进行突变也不希望它永远不会改变的代码可以使用IReadableFoo,而...
supercat 2012年

...要保留其内容的代码Foo可以使用ImmutableFoowhile代码,要操纵它的可以使用MutableFoo。给出任何类型的代码都IReadableFoo应该能够获得可变或不可变的版本。这样的框架会很不错,但是不幸的是,我找不到以通用方式进行设置的任何不错的方法。如果有一种一致的方法可以为类创建只读包装,则可以将此类内容与用于组合ICloneable<T>保存T' 的类的不可变副本一起使用。
2012年

@supercat如果要克隆a List<T>,以使克隆的对象List<T>是一个新集合,其中包含指向原始集合中所有相同对象的指针,则有两种简单的方法而无需使用ICloneable<T>。第一个是Enumerable.ToList()扩展方法:List<foo> clone = original.ToList();第二个是采用的List<T>构造函数IEnumerable<T>List<foo> clone = new List<foo>(original);我怀疑扩展方法可能只是在调用构造函数,但是这两种方法都能满足您的要求。;)
CptRobby

12

我认为“为什么”这个问题是不必要的。有很多接口/类/等等...这是非常有用的,但不是.NET Frameworku基础库的一部分。

但是,主要是您可以自己做。

public interface ICloneable<T> : ICloneable {
    new T Clone();
}

public abstract class CloneableBase<T> : ICloneable<T> where T : CloneableBase<T> {
    public abstract T Clone();
    object ICloneable.Clone() { return this.Clone(); }
}

public abstract class CloneableExBase<T> : CloneableBase<T> where T : CloneableExBase<T> {
    protected abstract T CreateClone();
    protected abstract void FillClone( T clone );
    public override T Clone() {
        T clone = this.CreateClone();
        if ( object.ReferenceEquals( clone, null ) ) { throw new NullReferenceException( "Clone was not created." ); }
        return clone
    }
}

public abstract class PersonBase<T> : CloneableExBase<T> where T : PersonBase<T> {
    public string Name { get; set; }

    protected override void FillClone( T clone ) {
        clone.Name = this.Name;
    }
}

public sealed class Person : PersonBase<Person> {
    protected override Person CreateClone() { return new Person(); }
}

public abstract class EmployeeBase<T> : PersonBase<T> where T : EmployeeBase<T> {
    public string Department { get; set; }

    protected override void FillClone( T clone ) {
        base.FillClone( clone );
        clone.Department = this.Department;
    }
}

public sealed class Employee : EmployeeBase<Employee> {
    protected override Employee CreateClone() { return new Employee(); }
}

任何可继承类的可行克隆方法都必须使用Object.MemberwiseClone作为起点(否则要使用反射),因为否则无法保证克隆的类型与原始对象的类型相同。如果您感兴趣的话,我有一个很好的模式。
supercat

@supercat为什么不回答呢?
nawfal 2013年

“有很多接口/类/等等……这非常有用,但不属于.NET Frameworku基础库的一部分。” 能给我们例子吗?
克里斯蒂克'16

1
@Krythic即:高级数据结构,如b树,红黑树,圆形缓冲区。在'09中,没有元组,通用弱引用,并发集合...
TcK

10

如果需要,可以自己编写接口,这很容易:

public interface ICloneable<T> : ICloneable
        where T : ICloneable<T>
{
    new T Clone();
}

我加入了一个摘要,因为由于版权保护,链接断开了……
Shimmy Weitzhandler 2010年

2
该接口应该如何工作?为了使克隆操作的结果可转换为类型T,要克隆的对象必须从T派生。无法设置这种类型约束。实际上,我只能看到iCloneable返回的结果是iCloneable类型。
超级猫

@supercat:是的,这正是Clone()返回的:一个实现ICloneable <T>的T。MbUnit使用此接口已有多年历史了,所以可以。至于实现,请看一下MbUnit的源代码。
Mauricio Scheffer 2010年

2
@Mauricio Scheffer:我知道发生了什么。没有类型实际实现iCloneable(of T)。而是,每种类型都实现iCloneable(本身)。我想那会行得通,尽管它所做的只是移动一个类型转换。太糟糕了,无法将一个字段声明为具有继承约束和接口。克隆方法返回一个半克隆(仅克隆作为受保护方法公开)基本类型但具有可克隆类型约束的对象会很有帮助。这将使对象接受基本类型的半克隆衍生物的可克隆衍生物。
超级猫

@Mauricio Scheffer:顺便说一句,我建议ICloneable(T的)也支持只读的Self属性(返回类型T)。这将允许一种方法接受Flone的ICloneable并将其(通过Self属性)用作Foo。
supercat 2010年

8

最近阅读过文章为什么复制对象是一件可怕的事情吗?,我认为这个问题需要进一步说明。这里的其他答案提供了很好的建议,但答案仍然不完整-为什么不ICloneable<T>呢?

  1. 用法

    因此,您有一个实现它的类。以前您有想要的方法ICloneable,但现在必须通用才能接受ICloneable<T>。您需要对其进行编辑。

    然后,您可能拥有一种检查object是否存在的方法is ICloneable。现在怎么办?您不能这样做,is ICloneable<>而且由于您不知道对象的类型为compile-type,因此无法使该方法通用。第一个真正的问题。

    因此,您需要同时具有ICloneable<T>和和ICloneable,前者要实现后者。因此,实现者将需要同时实现方法- object Clone()T Clone()。不,谢谢,我们已经有足够的乐趣了IEnumerable

    如前所述,继承也很复杂。虽然协方差似乎可以解决此问题,但派生类型需要实现ICloneable<T>自己的类型,但是已经有一个具有相同签名(基本上是=参数)的方法- Clone()基类。使新的克隆方法接口显式化是没有意义的,您将失去创建时寻求的优势ICloneable<T>。因此,添加new关键字。但不要忘记,您还需要重写基类Clone()(实现必须对所有派生类保持统一,即从每个克隆方法返回相同的对象,因此基克隆方法必须是virtual!)!但不幸的是,你不能同时overridenew具有相同签名的方法。选择第一个关键字,添加时您将失去想要达到的目标ICloneable<T>。选择第二个,您将破坏接口本身,使应该执行相同操作的方法返回不同的对象。

  2. 您想要ICloneable<T>舒适,但是舒适不是接口设计的目的,它们的含义是(通常是OOP)统一对象的行为(尽管在C#中,它仅限于统一外部行为,例如方法和属性,而不是统一对象)。他们的工作)。

    如果第一个原因还不能使您信服,则ICloneable<T>可以限制也可以从克隆方法返回的类型的对象上进行限制。但是,讨厌的程序员可以实现ICloneable<T>T不是实现它的类型。因此,要实现您的限制,您可以在通用参数上添加一个很好的约束:
    public interface ICloneable<T> : ICloneable where T : ICloneable<T>
    当然,没有的约束要严格一些where,您仍然不能限制T是实现接口的类型(可以派生自ICloneable<T>其他类型实现)。

    您会看到,即使达到这个目的也无法实现(原始方法ICloneable也失败了,没有接口可以真正限制实现类的行为)。

如您所见,这证明使通用接口既难以完全实现,也确实是不必要且无用的。

但是回到问题,您真正想要的是克隆对象时要舒适。有两种方法可以做到这一点:

其他方法

public class Base : ICloneable
{
    public Base Clone()
    {
        return this.CloneImpl() as Base;
    }

    object ICloneable.Clone()
    {
        return this.CloneImpl();
    }

    protected virtual object CloneImpl()
    {
        return new Base();
    }
}

public class Derived : Base
{
    public new Derived Clone()
    {
        return this.CloneImpl() as Derived;
    }

    protected override object CloneImpl()
    {
        return new Derived();
    }
}

该解决方案为用户提供了舒适感和预期的行为,但是实施起来也太长了。如果我们不想让“ comfortable”方法返回当前类型,则拥有just会容易得多public virtual object Clone()

因此,让我们看一下“最终”解决方案-C#中真正要给我们带来安慰的是什么?

扩展方法!

public class Base : ICloneable
{
    public virtual object Clone()
    {
        return new Base();
    }
}

public class Derived : Base
{
    public override object Clone()
    {
        return new Derived();
    }
}

public static T Copy<T>(this T obj) where T : class, ICloneable
{
    return obj.Clone() as T;
}

它的名称为Copy,不会与当前的Clone方法冲突(编译器更喜欢类型自己声明的方法,而不是扩展方法)。该class约束是有速度(不需要空检查等)。

我希望这可以弄清为什么不这样做的原因ICloneable<T>。但是,建议完全不要实施ICloneable


附录1:泛型的唯一可能用途ICloneable是用于值类型,在这种情况下它可能会绕过Clone方法的装箱,这意味着您没有装箱值。并且由于可以自动(浅)克隆结构,因此无需实现它(除非您明确指出它意味着深层复制)。
IllidanS4希望莫妮卡回到2014年

2

尽管这个问题很老(距撰写此答案5年之久),并且已经被回答了,但是我发现本文对这个问题的回答很好,请在此处查看

编辑:

这是回答问题的文章引文(请务必阅读全文,其中包括其他有趣的内容):

Internet上有许多参考文献,它们指向Brad Abrams于2003年在Microsoft任职时发表的一篇博客文章,其中讨论了有关ICloneable的一些想法。博客条目可在以下地址找到:实现ICloneable。尽管标题具有误导性,但此博客条目仍呼吁不要实施ICloneable,这主要是由于浅层/深层混乱。本文的结尾直接提出了一个建议:如果需要克隆机制,请定义自己的“克隆”或“复制”方法,并确保您清楚地记录是深拷贝还是浅拷贝。合适的模式是:public <type> Copy();


1

一个大问题是它们不能将T限制为同一类。例如,阻止您执行此操作的原因是:

interface IClonable<T>
{
    T Clone();
}

class Dog : IClonable<JackRabbit>
{
    //not what you would expect, but possible
    JackRabbit Clone()
    {
        return new JackRabbit();
    }

}

他们需要一个参数限制,例如:

interfact IClonable<T> where T : implementing_type

8
看上去还不如 class A : ICloneable { public object Clone() { return 1; } /* I can return whatever I want */ }
Nikola Novak

我真的不明白问题出在哪里。没有接口可以强制实现合理运行。即使ICloneable<T>可以约束T匹配其自身的类型,也不会强制实现Clone()返回与被克隆对象相似的任何远程对象。此外,我建议如果使用接口协方差,则最好是将实现的类ICloneable密封,使接口ICloneable<out T>包含Self可以返回自身的属性,以及……
supercat 2012年

...让消费者对ICloneable<BaseType>或施加或约束ICloneable<ICloneable<BaseType>>。所BaseType讨论的对象应具有protected用于克隆的方法,该方法将由实现的类型调用ICloneable。这种设计将允许以下可能性:a Container,a CloneableContainer,a FancyContainer和a CloneableFancyContainer,后者可在需要可克隆派生的Container或需要a的代码中使用FancyContainer(但不在乎是否可克隆)。
2012年

我赞成这种设计的原因是,是否可以合理地克隆类型的问题在某种程度上与它的其他方面正交。例如,一个FancyList类型可能会被合理地克隆,但是派生类可能会自动将其状态保存在磁盘文件中(在构造函数中指定)。无法克隆派生类型,因为其状态将附加到可变单例(文件)的状态上,但这不排除在需要a的大多数功能FancyList但不需要的地方使用派生类型克隆它。
2012年

+1-这个答案是我在这里看到的唯一可以真正回答这个问题的答案。
IllidanS4希望莫妮卡回到2014年

0

这是一个很好的问题……尽管您可以自己做:

interface ICloneable<T> : ICloneable
{
  new T Clone ( );
}

安德烈(Andrey)说这被认为是一个不好的API,但我还没有听说过该接口已被弃用。这会破坏大量的接口。Clone方法应该执行浅表复制。如果对象还提供了深层复制,则可以使用重载的Clone(bool deep)。

编辑:我用于“克隆”对象的模式,正在构造函数中传递原型。

class C
{
  public C ( C prototype )
  {
    ...
  }
}

这消除了任何潜在的冗余代码实现情况。BTW在谈到ICloneable的局限性时,不是由对象本身来决定是否应该执行浅克隆或深克隆,甚至是部分浅/部分深克隆?只要对象按预期工作,我们真的应该关心吗?在某些情况下,良好的Clone实现可能很好地包括浅层克隆和深层克隆。


“坏”并不意味着已弃用。它只是意味着“最好不要使用它”。在“我们将来会删除ICloneable接口”的意义上,不建议过时。他们只是鼓励您不要使用它,也不会使用泛型或其他新功能对其进行更新。
jalf

丹尼斯:请参阅马克的答案。协方差/继承问题使该接口在至少有一点点复杂的任何情况下几乎无法使用。
Tamas Czinege,09年

是的,我可以肯定地看到ICloneable的局限性,不过我很少需要使用克隆。
baretta
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.