복사 생성자와 Clone ()


119

C #에서 클래스에 (심층) 복사 기능을 추가하는 데 선호되는 방법은 무엇입니까? 복사 생성자를 구현해야합니까, 아니면 메서드 에서 파생되어 ICloneable구현해야 Clone()합니까?

: 부적합하다고 생각했기 때문에 괄호 안에 "깊이"라고 적었습니다. 분명히 다른 사람들이 동의하지 않기 때문에 복사 생성자 / 연산자 / 함수가 구현하는 복사 변형을 명확히해야하는지 물었 습니다 .

답변:


91

에서 파생해서는 안됩니다 ICloneable.

그 이유는 Microsoft가 .net 프레임 워크를 설계 할 때 Clone()메서드 ICloneable가 딥 또는 얕은 복제 인지 여부를 지정하지 않았기 때문에 호출자가 호출이 개체를 딥 복제할지 얕게 복제할지 알 수 없기 때문에 인터페이스가 의미 상 손상됩니다.

대신 (및 ) 메서드를 사용하여 자신의 IDeepCloneable(및 IShallowCloneable) 인터페이스를 정의해야 합니다.DeepClone()ShallowClone()

두 개의 인터페이스를 정의 할 수 있습니다. 하나는 강력한 유형의 복제를 지원하는 일반 매개 변수가있는 인터페이스이고 다른 하나는 서로 다른 유형의 복제 가능한 객체 컬렉션으로 작업 할 때 약한 유형의 복제 기능을 유지하지 않는 것입니다.

public interface IDeepCloneable
{
    object DeepClone();
}
public interface IDeepCloneable<T> : IDeepCloneable
{
    T DeepClone();
}

그런 다음 다음과 같이 구현합니다.

public class SampleClass : IDeepCloneable<SampleClass>
{
    public SampleClass DeepClone()
    {
        // Deep clone your object
        return ...;
    }
    object IDeepCloneable.DeepClone()   
    {
        return this.DeepClone();
    }
}

일반적으로 나는 의도를 매우 명확하게 유지하는 복사 생성자와 반대로 설명 된 인터페이스를 사용하는 것을 선호합니다. 복사 생성자는 아마도 딥 클론으로 간주 될 수 있지만 IDeepClonable 인터페이스를 사용하는 것만 큼 명확한 의도는 아닙니다.

이는 .net 프레임 워크 디자인 지침Brad Abrams의 블로그 에서 논의됩니다.

(프레임 워크 / 라이브러리가 아닌 애플리케이션을 작성하는 경우 팀 외부의 누구도 코드를 호출하지 않을 것임을 확신 할 수 있으며, 그다지 중요하지 않으며 의미 론적 의미를 할당 할 수 있습니다. .net ICloneable 인터페이스에 대한 "deepclone"이지만, 이것이 문서화되어 있고 팀 내에서 잘 이해되는지 확인해야합니다. 개인적으로 프레임 워크 지침을 고수합니다.)


2
인터페이스를 사용하려는 경우 DeepClone (of T) () 및 DeepClone (of T) (dummy as T)을 사용하는 것은 어떻습니까? 둘 다 T를 반환합니까? 후자의 구문을 사용하면 인수를 기반으로 T를 추론 할 수 있습니다.
supercat 2010-07-27

@supercat : 유형을 추론 할 수 있도록 더미 매개 변수가 있다는 말입니까? 내가 생각하는 옵션입니다. 유형을 자동으로 추론하기 위해 더미 매개 변수를 갖는 것이 마음에 들지 않습니다. 아마도 내가 당신을 오해하고있는 것 같습니다. (당신이 의미하는 바를 알 수 있도록 새 답변에 일부 코드를 게시 할 수 있습니다).
Simon P Stevens

@supercat : 유형 추론을 허용하기 위해 더미 매개 변수가 정확하게 존재합니다. 일부 코드는 유형이 무엇인지 (예 : 필드, 속성 또는 다른 클래스에서 반환되는 함수이기 때문에) 준비된 액세스없이 무언가를 복제하려고 할 수 있으며 더미 매개 변수를 사용하여 유형을 적절하게 추론 할 수 있습니다. 생각해 보면 인터페이스의 전체 요점이 딥 복제 가능한 컬렉션과 같은 것을 만드는 것이기 때문에 실제로 도움이되지 않을 것입니다.이 경우 유형은 컬렉션의 제네릭 유형이어야합니다.
supercat 2010-07-28

2
질문! 어떤 상황에서 비 제네릭 버전을 원하십니까? 나를 위해, 그것은 단지에 대한 의미가 IDeepCloneable<T>당신이 즉, 자신의 구현을 만들 경우 당신은 무엇 T를 알고 있기 때문에 ... 존재SomeClass : IDeepCloneable<SomeClass> { ... }
카일 바란

2
@Kyle은 복제 가능한 개체를 사용하는 방법이 있다고 말하면 MyFunc(IDeepClonable data)특정 유형뿐만 아니라 모든 복제 가능 항목에서 작동 할 수 있습니다. 또는 복제 가능 컬렉션이있는 경우. IEnumerable<IDeepClonable> lotsOfCloneables그런 다음 동시에 많은 개체를 복제 할 수 있습니다. 그래도 그런 종류의 것이 필요하지 않다면 비 일반적인 것을 버리십시오.
Simon P Stevens

33

C #에서 클래스에 (심층) 복사 기능을 추가하는 데 선호되는 방법은 무엇입니까? 복사 생성자를 구현해야합니까, 아니면 ICloneable에서 파생하여 Clone () 메서드를 구현해야합니까?

문제 ICloneable는 다른 사람들이 언급했듯이 딥 또는 얕은 사본인지 여부를 지정하지 않아 실제로 사용할 수 없으며 실제로 거의 사용되지 않는다는 것입니다. 또한 object많은 캐스팅이 필요하기 때문에 고통스러운을 반환합니다 . (그리고 질문에서 클래스를 구체적으로 언급했지만에서 구현 하려면 권투 ICloneablestruct필요합니다.)

복사 생성자도 ICloneable의 문제 중 하나를 겪습니다. 복사 생성자가 딥 또는 얕은 복사를 수행하는지 여부는 명확하지 않습니다.

Account clonedAccount = new Account(currentAccount); // Deep or shallow?

DeepClone () 메서드를 만드는 것이 가장 좋습니다. 이렇게하면 의도가 완벽하게 명확 해집니다.

이로 인해 정적 메서드인지 인스턴스 메서드인지에 대한 질문이 제기됩니다.

Account clonedAccount = currentAccount.DeepClone();  // instance method

또는

Account clonedAccount = Account.DeepClone(currentAccount); // static method

복제가 개체가 수행하는 작업이 아닌 개체에 수행되는 작업처럼 보이기 때문에 가끔 정적 버전을 약간 선호합니다. 두 경우 모두 상속 계층 구조의 일부인 개체를 복제 할 때 처리해야 할 문제가 있으며 이러한 문제를 해결하는 방법이 궁극적으로 설계를 주도 할 수 있습니다.

class CheckingAccount : Account
{
    CheckAuthorizationScheme checkAuthorizationScheme;

    public override Account DeepClone()
    {
        CheckingAccount clone = new CheckingAccount();
        DeepCloneFields(clone);
        return clone;
    }

    protected override void DeepCloneFields(Account clone)
    {
        base.DeepCloneFields(clone);

        ((CheckingAccount)clone).checkAuthorizationScheme = this.checkAuthorizationScheme.DeepClone();
    }
}

1
DeepClone () 옵션이 가장 좋은지 모르겠지만 기본 프로그래밍 언어 기능에 대해 존재하는 혼란스러운 상황을 강조하기 때문에 귀하의 답변을 많이 좋아합니다. 나는 그가 가장 좋아하는 옵션을 선택하는 것은 사용자에게 달려 있다고 생각합니다.
Dimitri C.

11
여기서 요점을 논하지는 않겠지 만, 제 생각에는 호출자가 Clone ()을 호출 할 때 깊거나 얕은 것에 대해 그다지 신경 쓰지 않아야합니다. 그들은 유효하지 않은 공유 상태가없는 클론을 얻고 있다는 것을 알아야합니다. 예를 들어, 딥 클론에서 모든 요소를 ​​딥 클론하고 싶지 않을 수도 있습니다. Clone 호출자가 신경 써야하는 것은 원본에 대한 유효하지 않거나 지원되지 않는 참조가없는 새 복사본을 얻는 것입니다. 방법 'DeepClone "을 호출하면 호출자에게 너무 많은 구현 세부 정보를 전달하는 것 같다.
zumalifeguard

1
정적 메서드에 의해 복사되는 대신 자신을 복제하는 방법을 알고있는 개체 인스턴스의 문제점은 무엇입니까? 이것은 생물학적 세포와 함께 항상 현실 세계에서 발생합니다. 여러분이이 글을 읽으면서 여러분의 몸에있는 세포들은 바로 지금 스스로 복제하고 있습니다. 정적 방법 옵션 인 IMO는 더 번거롭고 기능을 숨기는 경향이 있으며 다른 사람의 이익을 위해 "가장 놀라운"구현을 사용하지 않습니다.
Ken Beckett

8
@KenBeckett-복제가 객체에 대해 수행 된 것이라고 생각하는 이유는 객체가 "한 가지를 수행하고 잘 수행"해야하기 때문입니다. 일반적으로 자신을 복제하는 것은 클래스의 핵심 역량이 아니라 기능에 따라 달라집니다. BankAccount의 복제본을 만드는 것은 매우 잘 할 수있는 작업이지만 자체 복제본을 만드는 것은 은행 계좌의 기능이 아닙니다. 당신의 세포 예는 광범위하게 유익하지 않습니다. 왜냐하면 생식은 세포가 진화 한 것이 정확히하기 때문입니다. Cell.Clone은 좋은 인스턴스 메서드이지만 다른 대부분의 경우에는 그렇지 않습니다.
Jeffrey L Whitledge 2012

23

복제 메서드를 readonly사용하면 대신 생성자를 사용했다면 가능했을 수있는 필드 를 만들 수 없기 때문에 주로 복제 메서드보다 복사 생성자를 사용하는 것이 좋습니다 .

다형성 복제가 필요한 경우 복사 생성자를 호출하여 구현하는 기본 클래스에 abstract또는 virtual Clone()메서드를 추가 할 수 있습니다 .

두 종류 이상의 복사 (예 : deep / shallow)가 필요한 경우 복사 생성자에서 매개 변수를 사용하여 지정할 수 있지만, 경험상 일반적으로 딥 복사와 얕은 복사의 혼합이 필요합니다.

전의:

public class BaseType {
   readonly int mBaseField;

   public BaseType(BaseType pSource) =>
      mBaseField = pSource.mBaseField;

   public virtual BaseType Clone() =>
      new BaseType(this);
}

public class SubType : BaseType {
   readonly int mSubField;

   public SubType(SubType pSource)
   : base(pSource) =>
      mSubField = pSource.mSubField;

   public override BaseType Clone() =>
      new SubType(this);
}

8
+1 다형성 클로닝을 처리합니다. 복제의 중요한 응용.
samis dec.

18

보호 된 복사 생성자를 사용하여 clone ()을 구현 해야한다는 훌륭한 주장이 있습니다.

보호 된 (비 공용) 복사 생성자를 제공하고 복제 메서드에서 호출하는 것이 더 좋습니다. 이를 통해 객체 생성 작업을 클래스 자체의 인스턴스에 위임 할 수 있으므로 확장 성을 제공하고 보호 된 복사 생성자를 사용하여 객체를 안전하게 생성 할 수 있습니다.

따라서 이것은 "대"질문이 아닙니다. 복사 생성자와 복제 인터페이스가 모두 필요할 수 있습니다.

(권장되는 공용 인터페이스는 생성자 기반이 아닌 Clone () 인터페이스입니다.)

다른 답변에서 명시적인 깊거나 얕은 논쟁에 사로 잡히지 마십시오. 현실 세계에서는 거의 항상 중간에있는 것입니다. 어느 쪽이든 호출자의 관심사가되어서는 안됩니다.

Clone () 계약은 단순히 "첫 번째 계약을 변경할 때 변경되지 않습니다"입니다. 얼마나 많은 그래프를 복사해야하는지 또는이를 발생시키기 위해 무한 재귀를 피하는 방법은 호출자와 관련이 없습니다.


"발신자의 관심사가되어서는 안됩니다". 더 이상 동의 할 수는 없지만 여기에서 List <T> aList = new List <T> (aFullListOfT)가 깊은 복사 (내가 원하는 것) 또는 얕은 복사 (깨질 수있는)를 수행하는지 알아 내려고합니다. 내 코드) 및 작업을 완료하기 위해 다른 방법을 구현해야하는지 여부!
ThunderGr

3
list <T>는 클론이 이해하기에는 너무 일반적입니다 (ha ha). 귀하의 경우에는 목록이 가리키는 객체가 아니라 목록의 사본 일뿐입니다. 새 목록을 조작해도 첫 번째 목록에는 영향을주지 않지만 개체는 동일하며 변경 불가능하지 않는 한 두 번째 집합의 목록을 변경하면 첫 번째 집합의 개체가 변경됩니다. 라이브러리에 list.Clone () 작업이있는 경우 "첫 번째 작업을 수행 할 때 변경되지 않음"과 같이 결과가 전체 복제가 될 것으로 예상해야합니다. 포함 된 개체에도 적용됩니다.
DanO 2014 년

1
List <T>는 콘텐츠를 올바르게 복제하는 것에 대해 당신보다 더 많이 알지 못할 것입니다. 기본 개체가 변경 불가능한 경우 이동하는 것이 좋습니다. 그렇지 않고 기본 객체에 Clone () 메서드가 있으면이를 사용해야합니다. List <T> aList = new List <T> (aFullListOfT.Select (t = t.Clone ())
DanO

1
하이브리드 접근 방식의 경우 +1. 두 접근 방식 모두 장점과 단점이 있지만 전체적인 이점이 더 많은 것 같습니다.
Kyle Baran

12

ICloneable을 구현 하는 것은 딥 또는 얕은 복사본인지 여부가 지정되지 않았기 때문에 권장 되지 않으므로 생성자를 선택하거나 직접 구현할 것입니다. 정말 분명하게 만들기 위해 DeepCopy ()라고 부를 수도 있습니다!


5
@Grant, 생성자는 어떻게 의도를 전달합니까? IOW, 객체가 생성자에 포함 된 경우 복사가 깊거나 얕습니까? 그렇지 않으면 DeepCopy () (또는 기타) 제안에 완전히 동의합니다.
Marc

7
생성자는 ICloneable 인터페이스만큼 명확하지 않다고 주장합니다. API 문서 / 코드를 읽어야 딥 클론을 수행하는지 여부를 알 수 있습니다. 메소드 로 IDeepCloneable<T>인터페이스를 정의합니다 DeepClone().
Kent Boogaart

2
@Jon-원자로가 끝나지 않았습니다!
Grant Crofton

@Marc, @Kent-맞습니다. 생성자도 좋은 생각이 아닙니다.
Grant Crofton

3
알 수없는 유형의 개체에서 iCloneable이 사용 된 곳을 본 사람이 있습니까? 인터페이스의 요점은 알 수없는 유형의 개체에 사용할 수 있다는 것입니다. 그렇지 않으면 단순히 Clone을 해당 유형을 반환하는 표준 메서드로 만들 수 있습니다.
supercat 2010-07-27

12

복사 생성자 및 추상 클래스에 문제가 발생합니다. 다음을 수행하고 싶다고 가정 해보십시오.

abstract class A
{
    public A()
    {
    }

    public A(A ToCopy)
    {
        X = ToCopy.X;
    }
    public int X;
}

class B : A
{
    public B()
    {
    }

    public B(B ToCopy) : base(ToCopy)
    {
        Y = ToCopy.Y;
    }
    public int Y;
}

class C : A
{
    public C()
    {
    }

    public C(C ToCopy)
        : base(ToCopy)
    {
        Z = ToCopy.Z;
    }
    public int Z;
}

class Program
{
    static void Main(string[] args)
    {
        List<A> list = new List<A>();

        B b = new B();
        b.X = 1;
        b.Y = 2;
        list.Add(b);

        C c = new C();
        c.X = 3;
        c.Z = 4;
        list.Add(c);

        List<A> cloneList = new List<A>();

        //Won't work
        //foreach (A a in list)
        //    cloneList.Add(new A(a)); //Not this time batman!

        //Works, but is nasty for anything less contrived than this example.
        foreach (A a in list)
        {
            if(a is B)
                cloneList.Add(new B((B)a));
            if (a is C)
                cloneList.Add(new C((C)a));
        }
    }
}

위의 작업을 수행 한 직후 인터페이스를 사용했거나 DeepCopy () / ICloneable.Clone () 구현에 만족하기를 바라기 시작합니다.


2
인터페이스 기반 접근 방식에 대한 좋은 주장.
DanO 2013 년

4

ICloneable의 문제는 의도와 일관성입니다. 딥 카피인지 얕은 카피인지는 분명하지 않습니다. 그 때문에 아마도 한 가지 또는 다른 방식으로 만 사용되지 않을 것입니다.

나는 그 문제에 대해 더 명확한 공개 복사 생성자를 찾지 못했습니다.

즉, 나는 당신을 위해 작동하고 의도를 전달하는 방법 시스템을 소개 할 것입니다 (다소 자체 문서화)


3

복사하려는 개체가 직렬화 가능하면 직렬화 및 역 직렬화하여 복제 할 수 있습니다. 그러면 각 클래스에 대해 복사 생성자를 작성할 필요가 없습니다.

지금은 코드에 액세스 할 수 없지만 다음과 같습니다.

public object DeepCopy(object source)
{
   // Copy with Binary Serialization if the object supports it
   // If not try copying with XML Serialization
   // If not try copying with Data contract Serailizer, etc
}

6
딥 클로닝을 구현하는 수단으로 직렬화를 사용하는 것은 딥 클론이 ctor 또는 메서드로 표시되어야하는지 여부에 대한 질문과 관련이 없습니다.
Kent Boogaart

1
또 다른 유효한 대안이라고 생각합니다. 나는 그가 딥 카피의 두 가지 방법에 국한되었다고 생각하지 않았습니다.
Shaun Bowe

5
@Kent Boogaart-OP가 "C #에서 클래스에 (깊은) 복사 기능을 추가하는 데 선호되는 방법은 무엇입니까?"라는 줄로 시작한다는 점을 감안할 때 Shaun이 다른 대안을 제공하기에 충분하다고 생각합니다. 특히 복제 기능을 구현하려는 많은 수의 클래스가있는 레거시 시나리오에서는이 트릭이 유용 할 수 있습니다. 직접 복제를 구현하는 것만 큼 가볍지는 않지만 그럼에도 불구하고 유용합니다. 사람들이 내 질문에 대해 "생각해 본 적이있는 ..."대안을 제안하지 않았다면 수년 동안 배운 것만 큼 많이 배웠을 것입니다.
Rob Levine

2

개발자로 정의해야하는 문제의 클래스의 복사 의미에 따라 다릅니다. 선택한 방법은 일반적으로 클래스의 의도 된 사용 사례를 기반으로합니다. 두 방법을 모두 구현하는 것이 합리적 일 수 있습니다. 그러나 둘 다 비슷한 단점을 공유합니다. 구현하는 복사 방법이 정확히 명확하지 않습니다. 이는 수업 문서에 명확하게 명시되어야합니다.

나를 위해 :

// myobj is some transparent proxy object
var state = new ObjectState(myobj.State);

// do something

myobject = GetInstance();
var newState = new ObjectState(myobject.State);

if (!newState.Equals(state))
    throw new Exception();

대신에:

// myobj is some transparent proxy object
var state = myobj.State.Clone();

// do something

myobject = GetInstance();
var newState = myobject.State.Clone();

if (!newState.Equals(state))
    throw new Exception();

더 명확한 의도의 진술로 보였습니다.


0

복제 가능한 개체에 대한 표준 패턴이 있어야한다고 생각하지만 패턴이 정확히 무엇이어야하는지 잘 모르겠습니다. 복제와 관련하여 세 가지 유형의 클래스가있는 것 같습니다.

  1. 딥 클로닝을 명시 적으로 지원하는 것
  2. 구성원 별 복제는 딥 복제로 작동하지만 명시적인 지원이 필요하지도 않고 필요하지도 않습니다.
  3. 유용하게 딥 클로닝 할 수없고 멤버 별 클로닝으로 인해 나쁜 결과가 발생하는 것.

내가 말할 수있는 한, 기존 객체와 동일한 클래스의 새 객체를 얻는 유일한 방법 (적어도 .net 2.0에서는)은 MemberwiseClone을 사용하는 것입니다. 좋은 패턴은 항상 현재 유형을 반환하는 "new"/ "Shadows"함수 Clone을 갖는 것 같습니다. 정의는 항상 MemberwiseClone을 호출 한 다음 보호 된 가상 서브 루틴 CleanupClone (originalObject)을 호출하는 것입니다. CleanupCode 루틴은 base.Cleanupcode를 호출하여 기본 유형의 복제 요구를 처리 한 다음 자체 정리를 추가해야합니다. 복제 루틴이 원래 객체를 사용해야하는 경우 유형 변환해야하지만 그렇지 않으면 MemberwiseClone 호출에서만 유형 변환이 수행됩니다.

불행히도, 유형 (2)가 아닌 (1) 유형의 가장 낮은 수준의 클래스는 하위 유형이 복제에 대한 명시적인 지원이 필요하지 않다고 가정하도록 코딩해야합니다. 나는 그것에 대해 어떤 방법도 보지 못한다.

그래도 정의 된 패턴을 갖는 것이없는 것보다 낫다고 생각합니다.

덧붙여서, 자신의 기본 유형이 iCloneable을 지원한다는 것을 알고 있지만 사용하는 함수의 이름을 모르는 경우 기본 유형의 iCloneable.Clone 함수를 참조 할 수있는 방법이 있습니까?


0

흥미로운 답변과 토론을 모두 읽은 경우에도 속성을 정확히 복사하는 방법 (모든 속성을 명시 적으로 복사하는 방법)에 대해 자문 해 볼 수 있습니다. 아니면 더 우아한 방법이 있습니까? 이것이 남은 질문이라면 다음을 살펴보십시오 (StackOverflow에서).

일반 확장 메서드를 사용하여 타사 클래스의 속성을 "심층적으로"복제하려면 어떻게해야합니까?

CreateCopy()모든 속성을 포함하는 객체의 "깊은"복사본을 만드는 확장 메서드를 구현하는 방법을 설명합니다 (속성별로 속성을 수동으로 복사 할 필요없이).

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.