Değer alıcısı ve işaretçi alıcısı


108

Benim için çok net değil, bu durumda her zaman bir işaretçi alıcı kullanmak yerine bir değer alıcısı kullanmak isterdim.
Dokümanlardan özetlemek için:

type T struct {
    a int
}
func (tv  T) Mv(a int) int         { return 0 }  // value receiver
func (tp *T) Mp(f float32) float32 { return 1 }  // pointer receiver

Dokümanlar "terimi, temel tipleri, dilimler ve küçük yapılar gibi türleri için, bir değer, alıcı çok ucuz yöntemin semantik bir işaretçi gerektirmedikçe bu nedenle, bir değer alıcı etkili ve açıktır." De der

İlk nokta bunun "çok ucuz" olduğunu söylüyor, ama soru daha çok işaretçi alıcıdan daha ucuz olması. Bu yüzden küçük bir kıyaslama yaptım (ana hattaki kod) bana gösterici alıcısının sadece bir string alanı olan bir yapı için bile daha hızlı olduğunu gösterdi. Sonuçlar şunlardır:

// Struct one empty string property
BenchmarkChangePointerReceiver  2000000000               0.36 ns/op
BenchmarkChangeItValueReceiver  500000000                3.62 ns/op


// Struct one zero int property
BenchmarkChangePointerReceiver  2000000000               0.36 ns/op
BenchmarkChangeItValueReceiver  2000000000               0.36 ns/op

(Düzenleme: Lütfen daha yeni go sürümlerinde ikinci noktanın geçersiz olduğunu unutmayın, yorumlara bakın) .
İkinci nokta diyor ki, "verimli ve net" ki bu daha çok zevk meselesi, değil mi? Şahsen ben tutarlılığı her yerde aynı şekilde kullanarak tercih ederim. Hangi anlamda verimlilik? performans açısından, işaretçi neredeyse her zaman daha verimli görünüyor. Tek bir int özelliğine sahip birkaç test çalışması, Değer alıcısının minimum avantajını gösterdi (0.01-0.1 ns / op aralığı)

Birisi bana bir değer alıcısının açıkça işaretçi alıcıdan daha mantıklı olduğu bir durumu söyleyebilir mi? Yoksa kıyaslamada yanlış bir şey mi yapıyorum, diğer faktörleri göz ardı ettim mi?


3
Benzer kıyaslamaları tek bir dize alanı ve ayrıca iki alanla çalıştırdım: string ve int alanları. Değer alıcısından daha hızlı sonuç aldım. BenchmarkChangePointerReceiver-4 10000000000 0.99 ns / op BenchmarkChangeItValueReceiver-4 10000000000 0.33 ns / op Bu Go 1.8 kullanıyor. Kriterleri en son çalıştırdığınızdan beri yapılan derleyici optimizasyonları olup olmadığını merak ediyorum. Daha fazla ayrıntı için özüne bakın.
pbitty

2
Haklısın. Orijinal kıyaslamamı Go1.9 kullanarak çalıştırdığımda, şimdi de farklı sonuçlar alıyorum. İşaretçi Alıcı 0.60 ns / op, Değer alıcı 0.38 ns / op
Chrisport

Yanıtlar:


118

SSS'nin tutarlılıktan bahsettiğini unutmayın

Sırada tutarlılık var. Türün bazı yöntemlerinin işaretçi alıcıları olması gerekiyorsa, geri kalanı da olmalıdır, bu nedenle yöntem kümesi türün nasıl kullanıldığına bakılmaksızın tutarlıdır. Ayrıntılar için yöntem kümeleri bölümüne bakın.

Bu ileti dizisinde bahsedildiği gibi :

İşaretçiler ve alıcılar için değerler hakkındaki kural, değer yöntemlerinin işaretçiler ve değerler üzerinde çağrılabilmesidir, ancak işaretçi yöntemleri yalnızca işaretçiler üzerinde çağrılabilir.

Şimdi:

Birisi bana bir değer alıcısının açıkça işaretçi alıcıdan daha mantıklı olduğu bir durumu söyleyebilir mi?

Kod İnceleme comment yardımcı olabilir:

  • Alıcı bir harita, işlev veya kanal ise, bunun için bir işaretçi kullanmayın.
  • Alıcı bir dilimse ve yöntem dilimi yeniden dilimlemez veya yeniden tahsis etmezse, bunun için bir işaretçi kullanmayın.
  • Yöntemin alıcıyı değiştirmesi gerekiyorsa, alıcının bir işaretçi olması gerekir.
  • Alıcı, bir sync.Mutexveya benzer bir senkronizasyon alanı içeren bir yapı ise , alıcının kopyalamayı önlemek için bir işaretçi olması gerekir.
  • Alıcı büyük bir yapı veya dizi ise, bir işaretçi alıcı daha verimlidir. Ne kadar büyük? Tüm öğelerini argüman olarak yönteme aktarmaya eşdeğer olduğunu varsayalım. Bu çok büyük geliyorsa, alıcı için de çok büyük.
  • Aynı anda veya bu yöntemden çağrıldığında işlev veya yöntemler alıcıyı değiştirebilir mi? Bir değer türü, yöntem çağrıldığında alıcının bir kopyasını oluşturur, bu nedenle dış güncellemeler bu alıcıya uygulanmaz. Orijinal alıcıda değişikliklerin görünür olması gerekiyorsa, alıcının bir işaretçi olması gerekir.
  • Alıcı bir yapı, dizi veya dilim ise ve onun öğelerinden herhangi biri mutasyona uğramış olabilecek bir şeye göstericiyse, niyeti okuyucuya daha açık hale getireceği için bir işaretçi alıcıyı tercih edin.
  • Alıcı, doğal olarak bir değer türü olan (örneğin, time.Timetür gibi bir şey ), değiştirilebilir alanları ve işaretçileri olmayan veya yalnızca int veya string gibi basit bir temel tür olan küçük bir dizi veya yapı ise , bir değer alıcı yapar anlamda .
    Bir değer alıcısı, üretilebilecek çöp miktarını azaltabilir; bir değer bir değer yöntemine aktarılırsa, yığın tahsis etmek yerine yığın üzerinde bir kopya kullanılabilir. (Derleyici bu tahsisattan kaçınma konusunda akıllı davranmaya çalışır, ancak her zaman başarılı olamaz.) Bu nedenle önce profil oluşturmadan bir değer alıcı türü seçmeyin.
  • Son olarak, şüphe duyduğunuzda bir işaretçi alıcısı kullanın.

Kalın yazılan kısım, örneğin net/http/server.go#Write():

// Write writes the headers described in h to w.
//
// This method has a value receiver, despite the somewhat large size
// of h, because it prevents an allocation. The escape analysis isn't
// smart enough to realize this function doesn't mutate h.
func (h extraHeader) Write(w *bufio.Writer) {
...
}

16
The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers Aslında doğru değil. Hem değer alıcısı hem de işaretçi alıcı yöntemleri, doğru yazılmış bir işaretçi veya işaretçi olmayan üzerinde çağrılabilir. Yöntemin ne çağrıldığına bakılmaksızın, yöntem gövdesi içinde alıcının tanımlayıcısı, bir değer alıcısı kullanıldığında bir kopya değerine ve bir işaretçi alıcısı kullanıldığında bir işaretçiye atıfta bulunur
Hart Simha

3
Burada harika bir açıklama var "Eğer x adreslenebilirse ve & x'in yöntem seti m içeriyorsa, xm (), (& x) .m () için kısaltmadır."
tera


4
Harika cevap, ancak şu noktaya kesinlikle katılmıyorum: "niyeti daha açık hale getireceği için", NOPE, temiz bir API, argüman olarak X ve bir dönüş değeri olarak Y açık bir niyettir. İşaretçiyle bir Struct'ı geçirmek ve tüm özniteliklerin nelerin değiştirildiğini kontrol etmek için kodu dikkatlice okumak için zaman harcamak, açık ve sürdürülebilir olmaktan uzaktır.
Lukas Lukac

@HartSimha Yukarıdaki yazının, işaretçi alıcı yöntemlerinin değer türleri için "yöntem kümesi" içinde olmadığına işaret ettiğini düşünüyorum. Bağlantılı oyun, aşağıdaki satırı ekleyerek derleme hatasına neden olur: Int(5).increment_by_one_ptr(). Benzer şekilde, yöntemi tanımlayan bir özellik, bir increment_by_one_ptrtür değeri ile tatmin olmayacaktır Int.
Gaurav Agarwal

16

@VonC'ye ek olarak harika, bilgilendirici bir cevap eklemek için.

Proje büyüdükten, eski geliştiriciler ayrıldıktan ve yenisi geldiğinde kimsenin bakım maliyetinden gerçekten bahsetmediğine şaşırdım. Git elbette genç bir dil.

Genel olarak, yapabildiğim zaman işaretçilerden kaçınmaya çalışıyorum ama onların yeri ve güzellikleri var.

Şu durumlarda işaretçiler kullanıyorum:

  • büyük veri kümeleriyle çalışmak
  • yapı koruma durumuna sahip, örneğin TokenCache,
    • TÜM alanların ÖZEL olduğundan emin olurum, etkileşim yalnızca tanımlı yöntem alıcıları aracılığıyla mümkündür
    • Bu işlevi herhangi bir gorutine devretmiyorum

Örneğin:

type TokenCache struct {
    cache map[string]map[string]bool
}

func (c *TokenCache) Add(contract string, token string, authorized bool) {
    tokens := c.cache[contract]
    if tokens == nil {
        tokens = make(map[string]bool)
    }

    tokens[token] = authorized
    c.cache[contract] = tokens
}

İşaretçilerden kaçınmamın nedenleri:

  • işaretçiler eşzamanlı olarak güvenli değildir (GoLang'ın tüm noktası)
  • bir kez işaretçi alıcı, her zaman alıcı işaretçi (tüm Struct tutarlılık yöntemleri için)
  • Muteksler, "değer kopyalama maliyeti" ile karşılaştırıldığında kesinlikle daha pahalı, daha yavaş ve bakımı daha zordur
  • "değer kopyalama maliyeti" nden bahsetmişken, bu gerçekten bir sorun mu? Erken optimizasyon tüm kötülüklerin köküdür, daha sonra her zaman işaretçiler ekleyebilirsiniz
  • doğrudan, bilinçli olarak beni küçük Yapılar tasarlamaya zorluyor
  • işaretçilerden çoğunlukla saf işlevler net bir niyetle ve açık bir G / Ç ile tasarlanarak kaçınılabilir
  • Çöp toplama inandığım işaretçilerle daha zor
  • kapsülleme, sorumluluklar hakkında tartışmak daha kolay
  • basit tutun, aptalca (evet, işaretçiler yanıltıcı olabilir çünkü bir sonraki projenin geliştiricisini asla bilemezsiniz)
  • birim testi pembe bahçede yürümek gibidir (sadece slovakça ifade mi?)
  • koşullar varsa NIL yok (bir göstericinin beklendiği yerde NIL geçilebilir)

Genel kuralım, mümkün olduğunca çok sayıda kapsüllenmiş yöntem yazın, örneğin:

package rsa

// EncryptPKCS1v15 encrypts the given message with RSA and the padding scheme from PKCS#1 v1.5.
func EncryptPKCS1v15(rand io.Reader, pub *PublicKey, msg []byte) ([]byte, error) {
    return []byte("secret text"), nil
}

cipherText, err := rsa.EncryptPKCS1v15(rand, pub, keyBlock) 

GÜNCELLEME:

Bu soru, konuyu daha fazla araştırmam ve bununla ilgili bir blog yazısı yazmam için bana ilham verdi https://medium.com/gophersland/gopher-vs-object-oriented-golang-4fa62b88c701


Burada söylediklerinizin% 99'unu seviyorum ve kesinlikle katılıyorum. Bununla birlikte, örneğinizin, amacınızı açıklamanın en iyi yolu olup olmadığını merak ediyorum. TokenCache esasen bir harita değildir (@VonC'den - "eğer alıcı bir harita, işlev veya kanal ise, bunun için bir işaretçi kullanmayın"). Haritalar referans türleri olduğundan, "Add ()" i bir işaretçi alıcısı yaparak ne kazanırsınız? TokenCache'nin herhangi bir kopyası aynı haritayı referans alacaktır. Bu Go oyun alanını görün - play.golang.com/p/Xda1rsGwvhq
Zengin

Uyumlu olduğumuza sevindim. Harika nokta. Aslında, sanırım bu örnekte işaretçi kullandım çünkü onu TokenCache'nin o haritadan daha fazla şeyi işlediği bir projeden kopyaladım. Ve bir yöntemde bir işaretçi kullanırsam, hepsinde onu kullanırım. İmleci bu belirli SO örneğinden kaldırmayı öneriyor musunuz?
Lukas Lukac

LOL, tekrar kopyala / yapıştır ihtarları! 😉 IMO, içine düşmesi kolay bir tuzağı gösterdiği için olduğu gibi bırakabilir veya haritayı durumu ve / veya büyük bir veri yapısını gösteren bir şeyle değiştirebilirsiniz.
Zengin

Pekala, yorumları okuyacaklarından eminim ... Not: Zengin, argümanlarınız makul görünüyor, beni LinkedIn'e ekleyin (profilimdeki bağlantı) mutlu bir şekilde bağlanın.
Lukas Lukac
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.