Swift dizi atamasının tutarsız olmasının bir nedeni var mı (ne referans ne de derin bir kopya)?


216

Belgeleri okuyorum ve dilin bazı tasarım kararlarında sürekli başımı sallıyorum. Ama beni gerçekten şaşırtan şey dizilerin nasıl ele alındığı.

Oyun alanına koştum ve denedim. Siz de deneyebilirsiniz. İlk örnek:

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

İşte ave bher ikisi de [1, 42, 3], kabul edebileceğim. Dizilere referans verilir - Tamam!

Şimdi bu örneğe bakın:

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

colduğunu [1, 2, 3, 42]AMA dolduğunu [1, 2, 3]. Yani, dson örnekteki değişikliği gördü ancak bunu bu örnekte görmüyor. Dokümantasyonda bunun nedeni uzunluğun değiştiğidir.

Şimdi, buna ne dersiniz:

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

eolduğu [4, 5, 3]serin olduğu. Çok endeksli bir değiştirme yapmak güzel, ancak fSTILL uzunluk değişmemiş olsa bile değişikliği görmüyor.

Özetlemek gerekirse, 1 öğeyi değiştirirseniz birden çok öğeyi değiştirir veya öğe eklerseniz, bir diziye yapılan genel başvurular değişiklikleri görür.

Bu benim için çok kötü bir tasarım gibi görünüyor. Bunu düşünmekte haklı mıyım? Dizilerin neden böyle davranması gerektiğini anlamamam için bir neden var mı?

EDIT : Diziler değişti ve şimdi değer semantiği var. Çok daha aklı başında!


95
Kayıt için, bu sorunun kapatılması gerektiğini düşünmüyorum. Swift yeni bir dildir, bu yüzden hepimizin öğrendiği gibi bir süre böyle sorular olacak. Bu soruyu çok ilginç buluyorum ve umarım birisinin savunma konusunda zorlayıcı bir davası olur.
Joel Berger

4
@Joel Fine, programcılara sorun, Stack Overflow belirli açılmamış programlama problemleri içindir.
bjb568

21
@ bjb568: Bu bir fikir değil. Bu soru gerçeklerle cevaplanmalıdır. Bazı Swift geliştiricileri gelir ve "X, Y ve Z için böyle yaptık" diye cevaplarsa, bu doğrudur. X, Y ve Z ile anlaşamayabilirsiniz, ancak X, Y ve Z için bir karar verildiyse, bu sadece dilin tasarımının tarihsel bir gerçeğidir. Niye std::shared_ptratomik olmayan bir versiyona sahip olmadığını sorduğumda olduğu gibi , fikre değil, gerçeğe dayanan bir cevap vardı (gerçek şu ki, komite bunu düşündü, ancak çeşitli nedenlerle istemedi).
Cornstalks

7
@JasonMArcher: Sadece son paragraf görüşe dayanır (evet, belki kaldırılmalıdır). Sorunun asıl başlığı (asıl sorunun kendisi olarak aldığım) gerçeklerle cevaplanabilir. Orada olan diziler çalıştıkları şekilde çalışmak üzere dizayn edilmiştir bir sebep.
Cornstalks

7
Evet, API-Beast'in dediği gibi, buna genellikle "Yarı-Kıçlı Kopyalama Dili Tasarımı" denir.
R. Martinho Fernandes

Yanıtlar:


109

Not bu dizi anlam ve sözdizimi Xcode beta 3 sürümünde değiştirildi ( blog yazısı sorusu artık geçerli nedenle,). Aşağıdaki yanıt beta 2'ye uygulanmıştır:


Performans nedenleriyle. Temel olarak, dizileri olabildiğince kopyalamaktan kaçınırlar (ve "C benzeri performans" iddia ederler). Dil kitabını alıntılamak :

Diziler için, kopyalama yalnızca dizinin uzunluğunu değiştirme potansiyeli olan bir eylem gerçekleştirdiğinizde gerçekleşir. Buna, öğe ekleme, ekleme veya kaldırma veya dizideki bir dizi öğeyi değiştirmek için aralıklı bir alt simge kullanma da dahildir.

Bunun biraz kafa karıştırıcı olduğunu kabul ediyorum, ancak en azından nasıl çalıştığının açık ve basit bir açıklaması var.

Bu bölüm ayrıca, bir diziye benzersiz bir şekilde başvuruda bulunulmasının nasıl sağlanacağı, dizilerin nasıl zorla kopyalanacağı ve iki dizinin depolamayı paylaşıp paylaşmadığı nasıl denetleneceği hakkında bilgiler içerir.


61
Tasarımda hem paylaştığınız hem de BÜYÜK kırmızı bir bayrağı kopyaladığınızı görüyorum.
Cthutu

9
Doğru. Bir mühendis bana dil tasarımı için bunun arzu edilmediğini ve Swift'e gelecek güncellemelerde "düzeltmeyi" umdukları bir şey olduğunu açıkladı. Radarlarla oy verin.
Erik Kerber

2
Bu sadece Linux alt işlem bellek yönetiminde yazma üzerine kopyalama (COW) gibi bir şey, değil mi? Belki de uzunluk-uzunluk değişikliği (COLA) olarak adlandırabiliriz. Bunu olumlu bir tasarım olarak görüyorum.
14'te

3
@justhalf SO'ya gelen ve dizilerinin neden paylaşılmadığını / paylaşılmadığını soran (sadece daha az net bir şekilde) karışık yeni başlayanları tahmin edebilirim.
John Dvorak

11
@justhalf: COW zaten modern dünyada bir kötümser ve ikincisi, COW sadece uygulama amaçlı bir tekniktir ve bu COLA şeyler tamamen rastgele paylaşım ve paylaşımsızlığa yol açar.
Köpek yavrusu

25

Swift dilinin resmi belgelerinden :

Alt simge sözdizimi ile yeni bir değer ayarladığınızda dizinin kopyalanmadığını unutmayın, çünkü alt simge sözdizimi ile tek bir değer ayarlamak dizinin uzunluğunu değiştirme potansiyeline sahip değildir. Ancak, diziye yeni bir öğe eklerseniz , dizinin uzunluğunu değiştirirsiniz . Bu, Swift'ten yeni değeri eklediğiniz noktada dizinin yeni bir kopyasını oluşturmasını ister . Bundan böyle, a dizinin ayrı, bağımsız bir kopyasıdır .....

Bu belgelerdeki Diziler için Atama ve Kopyalama Davranışı bölümünün tamamını okuyun . Dizideki bir dizi öğeyi değiştirdiğinizde dizinin tüm öğeler için kendisinin bir kopyasını aldığını göreceksiniz .


4
Teşekkürler. Soruma bu metinden belirsiz bir şekilde bahsettim. Ancak bir alt simge aralığını değiştirmenin uzunluğu değiştirmediği ve hala kopyalandığı bir örnek gösterdim. Dolayısıyla, bir kopya istemiyorsanız, onu teker teker değiştirmeniz gerekir.
Cthutu

21

Davranış Xcode 6 beta 3 ile değişti. Diziler artık referans türü değil ve yazma üzerine kopyaya sahip mekanizmasına sahip. bir kopya değiştirilecek.


Eski cevap:

Diğerlerinin de belirttiği gibi, Swift , bir seferde tek dizinler için değerler değiştirildiğinde dahil olmak üzere, dizileri kopyalamaktan kaçınmaya çalışır .

Bir dizi değişkeninin (!) Benzersiz olduğundan, başka bir değişkenle paylaşılmadığından emin olmak istiyorsanız, unshareyöntemi çağırabilirsiniz . Bu, yalnızca bir referansı olmadığı sürece diziyi kopyalar. Tabii ki copy, her zaman bir kopya oluşturacak yöntemi de çağırabilirsiniz , ancak aynı dizide başka bir değişkenin tutulmadığından emin olmak için paylaşımsız tercih edilir.

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

hmm, benim için bu unshare()yöntem tanımsız.
Hlung

1
@ Beta 3'te kaldırıldı, cevabımı güncelledim.
Pascal

12

Davranış, Array.Resize.NET yöntemine son derece benzer . Neler olup bittiğini anlamak için,. jetonun C, C ++, Java, C # ve Swift'de .

C'de bir yapı, değişkenlerin toplanmasından başka bir şey değildir. .Bir yapı türünün değişkenine uygulanması, yapı içinde depolanan bir değişkene erişecektir. Nesneye işaretçiler , değişkenlerin toplanmasını tutmaz , onları tanımlar . Birinin bir yapıyı tanımlayan bir işaretçisi varsa,-> operatör işaretçi tarafından tanımlanan yapı içinde saklanan bir değişkene erişmek için kullanılabilir.

C ++ 'da, yapılar ve sınıflar yalnızca değişkenleri toplamakla kalmaz, aynı zamanda bunlara kod da ekleyebilir. Kullanımı .bir değişkene hareket etme bu yöntemi soracaktır bir yöntemi çağırmak için değişkenin kendisi içeriğine üzerine ; ->bir nesneyi tanımlayan bir değişken üzerinde kullanılması , bu yöntemin değişken tarafından tanımlanan nesne üzerinde hareket etmesini ister .

Java'da, tüm özel değişken türleri nesneleri tanımlar ve bir değişkenin üzerine bir yöntem çağırmak, yönteme değişken tarafından hangi nesnenin tanımlandığını bildirir. Değişkenler herhangi bir bileşik veri türünü doğrudan tutamaz veya bir yöntemin çağrıldığı bir değişkene erişebilmesi için herhangi bir yol yoktur. Bu kısıtlamalar, anlamsal olarak sınırlansa da, çalışma zamanını büyük ölçüde basitleştirir ve bayt kodu doğrulamasını kolaylaştırır; bu tür basitleştirmeler, piyasanın bu tür sorunlara duyarlı olduğu bir zamanda Java'nın kaynak yükünü azaltmış ve böylece pazarda çekiş sağlamasına yardımcı olmuştur. Ayrıca ., C veya C ++ 'da kullanılana benzer bir jetona ihtiyaç olmadığı anlamına da geliyorlardı . Her ne kadar Java başka bir amaçla gerekmediğinden kullanmış olabilir .-> , C ve C ++ ile aynı şekilde , içerik oluşturucular tek karakterli.

C # ve diğer .NET dillerinde, değişkenler nesneleri tanımlayabilir veya bileşik veri türlerini doğrudan tutabilir. Bileşik veri türünde bir değişken üzerinde kullanıldığında, değişkenin içeriğine. etki eder ; bir referans tipi değişkeni üzerinde kullanıldığında, tanımlanan nesneye etki eder.onun tarafından. Bazı işlemler için semantik ayrım özellikle önemli değildir, ancak diğerleri için önemlidir. En sorunlu durumlar, bileşik veri tipinin çağrıldığı değişkeni değiştirecek yönteminin salt okunur bir değişken üzerinde çağrıldığı durumlardır. Salt okunur bir değer veya değişken üzerinde bir yöntem çağrılmaya çalışılırsa, derleyiciler genellikle değişkeni kopyalar, yöntemin buna göre hareket etmesine izin verir ve değişkeni atar. Bu genellikle yalnızca değişkeni okuyan yöntemlerle güvenlidir, ancak ona yazılan yöntemlerle güvenli değildir. Ne yazık ki, .does henüz hangi yöntemlerin bu tür bir ikame ile güvenle kullanılabileceğini ve hangilerinin kullanılamayacağını gösteren herhangi bir araca sahip değildir.

Swift'te, toplamalar üzerindeki yöntemler, çağrıldıkları değişkeni değiştirip değiştirmeyeceklerini açıkça belirleyebilir ve derleyici, salt okunur değişkenler üzerinde mutasyon yöntemlerinin kullanılmasını yasaklar (daha sonra değişkenin geçici kopyalarını mutasyona uğratmak yerine) atılmalıdır). Bu ayrım nedeniyle, .çağrıldıkları değişkenleri değiştiren çağrı yöntemlerini kullanmak için Swift'te .NET'ten çok daha güvenlidir. Ne yazık ki, aynı .tokenin bir değişken tarafından tanımlanan harici bir nesneye etki etmek için kullanılması, karışıklık olasılığı anlamına gelir.

Bir zaman makinesi varsa ve C # ve / veya Swift'in yaratılmasına geri dönmüş olsaydı, diller .ve ->jetonları C ++ kullanımına çok daha yakın bir şekilde kullanmalarıyla bu tür sorunları çevreleyen karışıklığın çoğunu geriye dönük olarak önleyebilirdi . Her iki agrega ve başvuru türleri yöntemleri kullanabilir .üzerine hareket etmek değişken da çağrılan edildi, bunun üzerine ve ->bir üzerine hareket etmek değeri (kompozitler için) ya da (referans tipleri için) bu şekilde tanımlanan bir şey. Ancak her iki dil de bu şekilde tasarlanmamıştır.

C # 'da, çağrıldığı değişkeni değiştirmek için bir yöntemin normal uygulaması, değişkeni refparametre olarak bir yönteme geçirmektir . Böylece çağıran Array.Resize(ref someArray, 23);zaman someArraytanımlar 20 elemanlı bir dizi neden olacaktır someArrayorijinal dizi etkilemeden, 23 elemanlarının yeni bir dizi tanımlamak için. Bunun kullanımı ref, yöntemin çağrıldığı değişkeni değiştirmesi beklendiğini açıkça ortaya koymaktadır. Birçok durumda, statik yöntemler kullanmak zorunda kalmadan değişkenleri değiştirebilmek avantajlıdır; .Sözdizimi kullanarak anlamına gelen Swift adresleri . Dezavantajı, değişkenlerin hangi yöntemlere ve değerlerin hangi yöntemlere etki ettiğine açıklık getirmesidir.


5

Benim için sabitleri ilk önce değişkenlerle değiştirirseniz bu daha anlamlı olur:

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

İlk satırın boyutunu asla değiştirmesi gerekmez a. Özellikle, hiçbir zaman bellek ayırma işlemi yapmasına gerek yoktur. Değerine bakılmaksızın i, bu hafif bir işlemdir. Kaputun altında abir işaretçi olduğunu hayal ederseniz , sabit bir işaretçi olabilir.

İkinci satır çok daha karmaşık olabilir. Değerlerine bağlı ive j, bellek yönetimi yapmak gerekebilir. Bunun edizinin içeriğine işaret eden bir işaretçi olduğunu düşünüyorsanız, artık sabit bir işaretçi olduğunu varsayamazsınız; yeni bir bellek bloğu tahsis etmeniz, eski bellek bloğundan yeni bellek bloğuna veri kopyalamanız ve işaretçiyi değiştirmeniz gerekebilir.

Dil tasarımcıları (1) 'i olabildiğince hafif tutmaya çalıştılar. (2) yine de kopyalamayı içerebileceğinden, her zaman bir kopya yapmışsınız gibi davrandığı çözüme başvurmuşlardır.

Bu karmaşık, ama onlar bile daha gibi örneğin özel durumlarda ile komplike yapmadığını mutluyum i ve j derleme zamanı sabitleri ve (2) 'de derleyici sonucuna ulaşabilirse," e boyutu girmeyeceği anlamına değiştirin, o zaman kopyalamayız " .


Son olarak, Swift dilinin tasarım ilkelerini anlamama dayanarak, genel kuralların bence:

  • Sabitleri ( let) her zaman varsayılan olarak her yerde kullanın ve büyük sürprizler olmayacaktır.
  • Değişkenleri ( var) yalnızca kesinlikle gerekliyse kullanın ve bu gibi durumlarda değişikliklere dikkat edin, çünkü sürprizler olacaktır [burada: bazı durumlarda dizilerin garip örtülü kopyaları].

5

Bulduğum şey: Dizi, yalnızca işlemin dizinin uzunluğunu değiştirme potansiyeline sahipse , başvurulanın değiştirilebilir bir kopyası olacaktır . Son örneğinizde, f[0..2]birçoğu ile dizin oluşturma, işlemin uzunluğunu değiştirme potansiyeline sahiptir (kopyalara izin verilmiyor olabilir), bu yüzden kopyalanır.

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

8
"uzunluk değişti gibi muamele" Uzunluğu değiştiğinde kopyalanacaktı kavrayabilirim, ancak yukarıdaki alıntı ile birlikte, bu gerçekten endişe verici bir "özellik" olduğunu düşünüyorum ve bir çok insanın yanlış olacağını düşünüyorum
Joel Berger

25
Bir dilin yeni olması, göze çarpan iç çelişkiler içermesinin iyi olduğu anlamına gelmez.
Orbit'te Hafiflik Yarışları

Bu beta 3'te "düzeltildi", vardiziler artık tamamen değişebilir ve letdiziler tamamen değişmez.
Pascal

4

Delphi dizeleri ve dizileri tamamen aynı "özelliğe" sahipti. Uygulamaya baktığınızda, mantıklıydı.

Her değişken dinamik belleğe bir işarettir. Bu bellek, bir referans sayımı ve ardından dizideki verileri içerir. Böylece tüm diziyi kopyalamadan veya herhangi bir işaretçi değiştirmeden dizideki bir değeri kolayca değiştirebilirsiniz. Diziyi yeniden boyutlandırmak isterseniz, daha fazla bellek ayırmanız gerekir. Bu durumda, geçerli değişken yeni tahsis edilen belleği gösterecektir. Ancak, orijinal diziye işaret eden diğer tüm değişkenleri kolayca izleyemezsiniz, böylece onları yalnız bırakırsınız.

Elbette, daha tutarlı bir uygulama yapmak zor olmaz. Tüm değişkenlerin yeniden boyutlandırma görmesini istiyorsanız, bunu yapın: Her değişken, dinamik bellekte depolanan bir kapsayıcıya bir işaretçidir. Kapsayıcı tam olarak iki şeyi tutar, bir referans sayısı ve gerçek dizi verisine işaretçi. Dizi verileri ayrı bir dinamik bellek bloğunda saklanır. Şimdi dizi verilerine sadece bir işaretçi var, böylece bunu kolayca yeniden boyutlandırabilirsiniz ve tüm değişkenler değişikliği görecektir.



0

Bunun için .copy () kullanıyorum.

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

1
Kodunuzu çalıştırdığımda "Tür değeri" [Int] 'üye' kopya 'yok "
jreft56 16

0

Sonraki Swift sürümlerinde dizilerin davranışında herhangi bir değişiklik oldu mu? Ben sadece senin örneğini çalıştırıyorum:

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

Ve sonuçlarım [1, 42, 3] ve [1, 2, 3]

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.