[arrays] Swift 배열 할당이 일치하지 않는 이유가 있습니까 (참조 또는 딥 카피)?

나는 문서를 읽고 있으며 언어의 디자인 결정 중 일부에서 끊임없이 머리를 흔들고 있습니다. 그러나 실제로 당황한 것은 배열을 처리하는 방법입니다.

나는 놀이터로 달려 가서 이것들을 시험해 보았다. 당신도 그들을 시도 할 수 있습니다. 첫 번째 예 :

var a = [1, 2, 3]
var b = a
a[1] = 42
a
b

여기 ab모두 [1, 42, 3]내가 받아 들일 수있는가. 배열이 참조됩니다-OK!

이제이 예제를보십시오 :

var c = [1, 2, 3]
var d = c
c.append(42)
c
d

c입니다 [1, 2, 3, 42]하지만 d입니다 [1, 2, 3]. 즉, d마지막 예제에서 변경 사항을 보았지만이 예제에서는 변경되지 않았습니다. 설명서에 따르면 길이가 변경 되었기 때문입니다.

자, 이건 어때?

var e = [1, 2, 3]
var f = e
e[0..2] = [4, 5]
e
f

e 이다 [4, 5, 3] 멋지다. 다중 인덱스 대체를 사용하는 것이 좋지만 f길이가 변경되지 않은 경우에도 STILL에서 변경 사항을 볼 수 없습니다.

요약하면 배열에 대한 공통 참조는 하나의 요소를 변경하면 변경 사항을 볼 수 있지만 여러 요소를 변경하거나 항목을 추가하면 사본이 만들어집니다.

이것은 나에게 매우 나쁜 디자인처럼 보입니다. 나는 이것을 생각하는 것이 맞습니까? 왜 배열이 이런 식으로 작동해야하는지 알 수없는 이유가 있습니까?

편집 : 배열이 변경되었으며 이제 값 의미가 있습니다. 훨씬 더 제정신!



답변

참고 배열 의미 및 구문 엑스 코드 베타 3 버전 변경 하였다 ( 블로그 게시물 문제는 더 이상 적용되지 않도록). 다음 답변은 베타 2에 적용되었습니다.


성능상의 이유입니다. 기본적으로 배열은 가능한 한 배열을 복사하지 않으려 고합니다 (및 “C와 유사한 성능”). 언어 을 인용하려면 :

배열의 경우 배열 길이를 수정할 수있는 작업을 수행 할 때만 복사가 수행됩니다. 여기에는 항목을 추가, 삽입 또는 제거하거나 범위가 지정된 첨자를 사용하여 배열의 항목 범위를 대체하는 것이 포함됩니다.

나는 이것이 약간 혼란 스럽지만 적어도 그것이 어떻게 작동하는지에 대한 명확하고 간단한 설명이 있음에 동의합니다.

이 섹션에는 어레이가 고유하게 참조되는지 확인하는 방법, 어레이를 강제 복사하는 방법 및 두 어레이가 스토리지를 공유하는지 여부를 확인하는 방법에 대한 정보도 포함되어 있습니다.


답변

스위프트 언어의 공식 문서에서 :

첨자 구문으로 단일 값을 설정하면 배열의 길이를 변경할 가능성이 없으므로 첨자 구문으로 새 값을 설정하면 배열이 복사되지 않습니다. 그러나 배열에 새 항목을 추가 하면 배열의 길이를 수정합니다 . 그러면 새 값을 추가 할 때 Swift가 배열새 사본 을 작성하라는 프롬프트를 표시 합니다. 따라서 a는 배열의 독립적 인 독립 복사본입니다 …..

이 문서에서 어레이 할당 및 복사 동작 섹션 전체를 읽으십시오 . 당신은 발견 할 것이다 당신이 할 때 배열의 항목의 범위를 교체 한 후 배열이 모든 항목에 자신의 복사본을합니다.


답변

Xcode 6 베타 3에서 동작이 변경되었습니다. 배열은 더 이상 참조 형식이 아니며 쓰기시 복사 메커니즘을 갖습니다. 즉, 하나 또는 다른 변수에서 배열의 내용을 변경하자마자 배열이 복사되고 하나의 사본이 변경됩니다.


이전 답변 :

다른 사람들이 지적했듯이 Swift는 한 번 에 단일 인덱스의 값을 변경할 때를 포함하여 가능한 경우 배열 복사 하려고합니다 .

배열 변수 (!)가 고유한지 확인하려면 (예 : 다른 변수와 공유하지 않는 경우) unshare메소드를 호출 할 수 있습니다 . 이미 하나의 참조가없는 경우를 제외하고 배열을 복사합니다. 물론 copy메소드를 호출 할 수도 있습니다.이 메소드는 항상 사본을 작성하지만 다른 변수가 동일한 배열을 보유하지 않도록하려면 공유 해제 를 선호합니다.

var a = [1, 2, 3]
var b = a
b.unshare()
a[1] = 42
a               // [1, 42, 3]
b               // [1, 2, 3]


답변

동작은 Array.Resize.NET 의 방법 과 매우 유사합니다 . 무슨 일이 일어나고 있는지 이해하려면 역사를 살펴 보는 것이 도움이 될 수 있습니다.. C, C ++, Java, C # 및 Swift 토큰 .

C에서 구조는 변수의 집합에 지나지 않습니다. .구조 유형의 변수 에을 적용하면 구조 내에 저장된 변수에 액세스합니다. 객체에 대한 포인터 는 변수 집계를 보유 하지 않지만 식별 합니다. 구조를 식별하는 포인터가 있으면-> 연산자를 사용하여 포인터로 식별 된 구조 내에 저장된 변수에 액세스 할 수 있습니다.

C ++에서 구조와 클래스는 변수를 집계 할뿐만 아니라 코드를 첨부 할 수도 있습니다. .변수를 사용 하여 메소드를 호출하면 해당 메소드가 변수 자체의 내용에 따라 작동하도록 요청 합니다 . ->객체를 식별하는 변수를 사용 하면 해당 메소드가 객체에 작용하도록 요구합니다. 식별 변수에 의해.

Java에서 모든 사용자 정의 변수 유형은 단순히 객체를 식별하고 변수에 대해 메소드를 호출하면 변수로 식별 된 객체를 메소드에 알려줍니다. 변수는 복합 데이터 유형을 직접 보유 할 수 없으며 메소드가 호출 된 변수에 액세스 할 수있는 방법도 없습니다. 이러한 제한은 의미 상 제한적이지만 런타임을 크게 단순화하고 바이트 코드 유효성 검사를 용이하게합니다. 이러한 단순화는 시장이 그러한 문제에 민감한 시점에 Java의 리소스 오버 헤드를 줄임으로써 시장에서 관심을 끌 수 있도록 돕습니다. 그들은 또한 토큰과 같은 토큰이 필요하지 않다는 것을 의미했습니다.. C 또는 C ++에서 사용되는 . Java는 ->C 및 C ++과 같은 방식으로 사용할 수 있었지만 제작자는 단일 문자를 사용하기로 선택했습니다.. 다른 목적으로는 필요하지 않았기 때문입니다.

C # 및 기타 .NET 언어에서 변수는 개체를 식별하거나 복합 데이터 형식을 직접 보유 할 수 있습니다. 복합 데이터 유형의 변수에 사용될 때 변수의 내용.작용 합니다. 참조 유형의 변수에 사용될 때 식별 된 객체에 작용.그것에 의해. 어떤 종류의 연산들에있어서, 시맨틱 구별은 특별히 중요하지 않지만, 다른 것들에게는 중요하다. 가장 문제가되는 상황은 호출 된 변수를 수정하는 복합 데이터 유형의 메소드가 읽기 전용 변수에서 호출되는 상황입니다. 읽기 전용 값 또는 변수에서 메소드를 호출하려고 시도하는 경우, 컴파일러는 일반적으로 변수를 복사하고 메소드가 해당 조치를 취하게하고 변수를 버립니다. 이것은 일반적으로 변수를 읽는 방법으로는 안전하지만 변수에 쓰는 방법으로는 안전하지 않습니다. 불행히도 .does는 아직 어떤 방법으로 이러한 대체 방법을 안전하게 사용할 수 있고 어떤 방법을 사용할 수 없는지를 나타내는 수단이 없습니다.

Swift에서 집계의 메소드는 호출 된 변수를 수정할지 여부를 명시 적으로 표시 할 수 있으며 컴파일러는 변수의 임시 사본을 변경하지 않고 읽기 전용 변수에 대해 변경 메소드를 사용하지 못하게합니다. 버린다). 이러한 차이점으로 인해 .토큰을 사용하여 호출 된 변수를 수정하는 메소드를 호출하는 것이 Swift에서 .NET보다 훨씬 안전합니다. 불행히도, .변수에 의해 식별되는 외부 객체에 작용 하는 것과 동일한 토큰이 그 목적으로 사용 된다는 사실 은 혼란의 가능성이 남아 있음을 의미합니다.

타임머신이 있고 C # 및 / 또는 Swift의 생성으로 돌아 가면 언어 가 C ++ 사용에 훨씬 가까운 방식으로 언어 .->토큰을 사용하게하여 이러한 문제를 둘러싼 혼란을 소급 적으로 피할 수 있습니다. 모두 단위 및 참조 유형의 방법은 사용할 수 .에 따라 행동하는 변수 가 호출 된시와 ->에 따라 행동하는 (복합) 또는 (참조 유형)하여 확인 된 것. 그러나 어느 언어도 그렇게 설계되지 않았습니다.

C #에서 호출 된 변수를 수정하는 메소드의 일반적인 방법은 변수를 ref매개 변수로 메소드 에 전달 하는 것입니다. 따라서 20 개의 요소로 구성된 배열을 식별 Array.Resize(ref someArray, 23);할 때 호출 하면 원래 배열에 영향을주지 않으면 서 23 개의 요소로 구성된 새로운 배열을 식별하게됩니다. 이 메소드를 사용하면 메소드가 호출 된 변수를 수정해야합니다. 많은 경우 정적 메소드를 사용하지 않고 변수를 수정할 수있는 것이 유리합니다. 구문 을 사용하여 의미 스위프트 주소 . 단점은 변수에 어떤 방법이 작용하고 값에 어떤 방법이 작용하는지 명확하지 않다는 것입니다.someArraysomeArrayref.


답변

나에게 이것은 상수를 변수로 대체하면 더 합리적입니다.

a[i] = 42            // (1)
e[i..j] = [4, 5]     // (2)

첫 번째 줄은 크기를 변경할 필요가 없습니다 a. 특히 메모리 할당이 필요하지 않습니다. 의 값에 관계없이이 i작업은 간단한 작업입니다. 후드 아래에서 a포인터라고 생각하면 상수 포인터가 될 수 있습니다.

두 번째 줄은 훨씬 더 복잡 할 수 있습니다. i및 의 값에 따라 j메모리 관리를 수행해야 할 수도 있습니다. 이것이 e배열의 내용을 가리키는 포인터라고 생각하면 더 이상 포인터가 상수 포인터라고 가정 할 수 없습니다. 새 메모리 블록을 할당하고 이전 메모리 블록에서 새 메모리 블록으로 데이터를 복사 한 다음 포인터를 변경해야 할 수도 있습니다.

언어 설계자들은 가능한 한 (1) 경량을 유지하려고 시도한 것 같습니다. (2) 어쨌든 복사가 필요할 수 있기 때문에 항상 복사하는 것처럼 작동하는 솔루션에 의지했습니다.

이 방법은 복잡하지만 “(2) i 및 j가 컴파일 타임 상수이고 컴파일러가 e의 크기가 흐르지 않는다고 유추 할 수있는 경우와 같은 특수한 경우에는 더 복잡하지 않게되어 기쁩니다. 변경하지 않으면 복사하지 않습니다 “ .


마지막으로 Swift 언어의 디자인 원칙에 대한 이해를 바탕으로 일반적인 규칙은 다음과 같습니다.

  • 상수 사용 (let기본적으로 항상 )를 하면 큰 놀라움이 없습니다.
  • 변수 ( var)는 반드시 필요한 경우에만 사용 하고, 놀랍게도 여기에주의하십시오 (여기서는 : 일부 상황에서는 배열의 이상한 암시 적 사본).

답변

내가 찾은 것은 : 배열에 길이가 변경 될 가능성이있는 경우에만 배열이 참조 된 사본의 변경 가능한 사본이 됩니다 . 마지막 예제에서 f[0..2]많은 색인을 생성하면 작업의 길이가 변경 될 수 있으므로 중복이 허용되지 않을 수 있으므로 복사됩니다.

var e = [1, 2, 3]
var f = e
e[0..2] = [4, 5]
e // 4,5,3
f // 1,2,3


var e1 = [1, 2, 3]
var f1 = e1

e1[0] = 4
e1[1] = 5

e1 //  - 4,5,3
f1 // - 4,5,3


답변

델파이의 문자열과 배열은 정확히 같은 “기능”을 가졌습니다. 구현을 살펴보면 의미가 있습니다.

각 변수는 동적 메모리에 대한 포인터입니다. 해당 메모리에는 참조 카운트와 그 뒤에 배열의 데이터가 포함됩니다. 따라서 전체 배열을 복사하거나 포인터를 변경하지 않고도 배열의 값을 쉽게 변경할 수 있습니다. 배열의 크기를 조정하려면 더 많은 메모리를 할당해야합니다. 이 경우 현재 변수는 새로 할당 된 메모리를 가리 킵니다. 그러나 원래 배열을 가리키는 다른 모든 변수를 쉽게 추적 할 수 없으므로 혼자 두십시오.

물론보다 일관된 구현을하는 것은 어렵지 않습니다. 모든 변수가 크기를 조정하도록하려면 다음과 같이하십시오. 각 변수는 동적 메모리에 저장된 컨테이너에 대한 포인터입니다. 컨테이너에는 정확히 두 개의 항목, 참조 횟수와 실제 배열 데이터에 대한 포인터가 있습니다. 어레이 데이터는 별도의 동적 메모리 블록에 저장됩니다. 이제 배열 데이터에 대한 포인터가 하나뿐이므로 쉽게 크기를 조정할 수 있으며 모든 변수에 변경 사항이 표시됩니다.