ตัวรับค่าเทียบกับตัวรับตัวชี้


112

มันไม่ชัดเจนมากสำหรับฉันในกรณีนี้ฉันต้องการใช้ตัวรับค่าแทนการใช้ตัวรับตัวชี้เสมอ
สรุปจากเอกสาร:

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

เอกสารกล่าวว่านอกจากนี้ยังมี "สำหรับประเภทเช่นประเภทพื้นฐานชิ้นและ structs ขนาดเล็กรับคุ้มค่าราคาถูกมากดังนั้นถ้าความหมายของวิธีการที่ต้องใช้ตัวชี้รับค่ามีประสิทธิภาพและชัดเจน."

จุดแรกบอกว่า "ถูกมาก" แต่คำถามคือถูกกว่าแล้วตัวรับสัญญาณ ดังนั้นฉันจึงสร้างเกณฑ์มาตรฐานขนาดเล็ก(รหัสในส่วนสำคัญ)ซึ่งแสดงให้ฉันเห็นว่าตัวรับตัวชี้นั้นเร็วกว่าแม้ในโครงสร้างที่มีฟิลด์สตริงเพียงช่องเดียว นี่คือผลลัพธ์:

// 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

(แก้ไข: โปรดทราบว่าจุดที่สองกลายเป็นที่ไม่ถูกต้องในรุ่นใหม่ไปดูความเห็น)
ประเด็นที่สองกล่าวว่า "มีประสิทธิภาพและชัดเจน" ซึ่งเป็นเรื่องของรสนิยมมากกว่าใช่หรือไม่? โดยส่วนตัวแล้วฉันชอบความสม่ำเสมอโดยใช้วิธีเดียวกันทุกที่ ประสิทธิภาพในแง่ใด? ประสิทธิภาพที่ชาญฉลาดดูเหมือนว่าตัวชี้มักจะมีประสิทธิภาพมากกว่า การทดสอบเพียงไม่กี่ครั้งที่มีคุณสมบัติ int เดียวแสดงให้เห็นข้อได้เปรียบน้อยที่สุดของตัวรับค่า (ช่วง 0.01-0.1 ns / op)

ใครช่วยบอกฉันได้ไหมว่าตัวรับค่ามีความหมายมากกว่าตัวรับตัวชี้ หรือฉันทำอะไรผิดพลาดในเกณฑ์มาตรฐานฉันมองข้ามปัจจัยอื่นไปหรือเปล่า?


3
ฉันใช้การเปรียบเทียบที่คล้ายกันกับฟิลด์สตริงเดียวและยังมีสองฟิลด์: สตริงและฟิลด์อินเทอร์ ฉันได้ผลลัพธ์ที่เร็วกว่าจากตัวรับค่า BenchmarkChangePointerReceiver-4 10000000000 0.99 ns / op BenchmarkChangeItValueReceiver-4 10000000000 0.33 ns / op ซึ่งใช้ Go 1.8 ฉันสงสัยว่ามีการเพิ่มประสิทธิภาพคอมไพเลอร์หรือไม่ตั้งแต่คุณเรียกใช้เกณฑ์มาตรฐานครั้งล่าสุด ดูส่วนสำคัญสำหรับรายละเอียดเพิ่มเติม
pbitty

2
คุณถูก. การใช้เกณฑ์มาตรฐานเดิมของฉันโดยใช้ Go1.9 ตอนนี้ฉันได้ผลลัพธ์ที่แตกต่างกันเช่นกัน ตัวรับตัวชี้ 0.60 ns / op ตัวรับค่า 0.38 ns / op
Chrisport

คำตอบ:


121

โปรดทราบว่าคำถามที่พบบ่อยกล่าวถึงความสอดคล้อง

ต่อไปคือความสม่ำเสมอ หากวิธีการบางประเภทต้องมีตัวรับตัวชี้ส่วนที่เหลือก็ควรมีเช่นกันดังนั้นชุดวิธีการจึงสอดคล้องกันไม่ว่าจะใช้ชนิดใดก็ตาม ดูรายละเอียดในส่วนวิธีการตั้งค่า s

ดังที่กล่าวไว้ในหัวข้อนี้ :

กฎเกี่ยวกับพอยน์เตอร์เทียบกับค่าสำหรับตัวรับคือเมธอดค่าสามารถเรียกใช้กับพอยน์เตอร์และค่าได้ แต่เมธอดพอยน์เตอร์สามารถเรียกใช้กับพอยน์เตอร์เท่านั้น

ตอนนี้:

ใครช่วยบอกฉันได้ไหมว่าตัวรับค่ามีความหมายมากกว่าตัวรับตัวชี้

ความคิดเห็นรหัสตรวจสอบสามารถช่วย:

  • ถ้าผู้รับเป็นแผนที่ func หรือ chan อย่าใช้ตัวชี้ไปที่มัน
  • หากเครื่องรับเป็นชิ้นส่วนและวิธีการนั้นไม่ได้ปรับขนาดหรือจัดสรรชิ้นส่วนใหม่อย่าใช้ตัวชี้ไปที่ชิ้นส่วนนั้น
  • หากวิธีการนั้นจำเป็นต้องกลายพันธุ์เครื่องรับเครื่องรับจะต้องเป็นตัวชี้
  • ถ้าตัวรับเป็นโครงสร้างที่มีsync.Mutexฟิลด์ซิงโครไนซ์หรือคล้ายกันตัวรับต้องเป็นตัวชี้เพื่อหลีกเลี่ยงการคัดลอก
  • ถ้าตัวรับเป็นโครงสร้างหรืออาร์เรย์ขนาดใหญ่ตัวรับตัวชี้จะมีประสิทธิภาพมากกว่า ขนาดใหญ่ขนาดไหน? สมมติว่ามันเทียบเท่ากับการส่งผ่านองค์ประกอบทั้งหมดเป็นอาร์กิวเมนต์ของเมธอด หากรู้สึกว่าใหญ่เกินไปแสดงว่ามีขนาดใหญ่เกินไปสำหรับเครื่องรับ
  • ฟังก์ชั่นหรือวิธีการไม่ว่าจะพร้อมกันหรือเมื่อเรียกจากวิธีนี้จะกลายพันธุ์เครื่องรับได้หรือไม่? ชนิดค่าจะสร้างสำเนาของผู้รับเมื่อมีการเรียกใช้เมธอดดังนั้นการอัปเดตภายนอกจะไม่ถูกนำไปใช้กับตัวรับนี้ หากต้องมองเห็นการเปลี่ยนแปลงในเครื่องรับเดิมเครื่องรับต้องเป็นตัวชี้
  • หากตัวรับเป็นโครงสร้างอาร์เรย์หรือสไลซ์และองค์ประกอบใด ๆ ของมันเป็นตัวชี้ไปยังสิ่งที่อาจกลายพันธุ์ให้เลือกตัวรับตัวชี้เนื่องจากจะทำให้ความตั้งใจชัดเจนยิ่งขึ้นสำหรับผู้อ่าน
  • ถ้าตัวรับเป็นอาร์เรย์ขนาดเล็กหรือโครงสร้างที่เป็นประเภทค่าโดยธรรมชาติ (เช่นบางอย่างเช่นtime.Timeประเภท) โดยไม่มีฟิลด์ที่ไม่แน่นอนและไม่มีพอยน์เตอร์หรือเป็นเพียงประเภทพื้นฐานธรรมดาเช่น int หรือสตริงตัวรับค่าจะทำให้ ความรู้สึก .
    ตัวรับค่าสามารถลดปริมาณขยะที่สร้างขึ้นได้ ถ้าค่าถูกส่งไปยังเมธอด value สามารถใช้สำเนาบนกองแทนการจัดสรรบนฮีปได้ (คอมไพลเลอร์พยายามหลีกเลี่ยงการจัดสรรนี้อย่างชาญฉลาด แต่ก็ไม่สามารถทำได้สำเร็จเสมอไป) อย่าเลือกประเภทตัวรับค่าด้วยเหตุผลนี้โดยไม่ต้องทำโปรไฟล์ก่อน
  • สุดท้ายหากมีข้อสงสัยให้ใช้ตัวรับตัวชี้

พบส่วนที่เป็นตัวหนาเช่นใน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 ไม่จริงก็จริง ทั้งตัวรับค่าและวิธีการรับตัวชี้สามารถเรียกใช้บนตัวชี้ที่พิมพ์ถูกต้องหรือไม่ใช่ตัวชี้ ไม่ว่าจะเรียกใช้เมธอดใดตัวระบุของผู้รับจะอ้างถึงค่าโดยสำเนาเมื่อใช้ตัวรับค่าและตัวชี้เมื่อใช้ตัวรับตัวชี้: ดูplay.golang.org/p / 3WHGaAbURM
Hart Simha

3
มีคำอธิบายที่ดีที่นี่ "ถ้า x สามารถระบุแอดเดรสได้และชุดวิธีของ & x มี m, xm () คือชวเลขสำหรับ (& x) .m ()"
tera

@tera ใช่: ที่คุยกันที่stackoverflow.com/q/43953187/6309
VonC

4
คำตอบที่ดี แต่ฉันไม่เห็นด้วยอย่างยิ่งกับประเด็นนี้: "เนื่องจากจะทำให้เจตนาชัดเจนยิ่งขึ้น", NOPE, API ที่สะอาด, X เป็นอาร์กิวเมนต์และ Y เป็นค่าตอบแทนเป็นเจตนาที่ชัดเจน การส่งโครงสร้างโดยตัวชี้และใช้เวลาในการอ่านโค้ดอย่างละเอียดเพื่อตรวจสอบว่าแอตทริบิวต์ทั้งหมดที่ได้รับการแก้ไขนั้นยังห่างไกลจากความชัดเจนและบำรุงรักษาได้
Lukas Lukac

@HartSimha ฉันคิดว่าโพสต์ด้านบนชี้ให้เห็นว่าเมธอดตัวรับตัวชี้ไม่ได้อยู่ใน "วิธีการตั้งค่า" สำหรับประเภทค่า Int(5).increment_by_one_ptr()ในสนามเด็กเล่นที่เชื่อมโยงของคุณเพิ่มบรรทัดต่อไปนี้จะส่งผลให้เกิดความผิดพลาดในการรวบรวม: ในทำนองเดียวกันลักษณะที่กำหนดวิธีการที่จะไม่ได้รับความพึงพอใจที่มีค่าของประเภทincrement_by_one_ptr Int
Gaurav Agarwal

18

หากต้องการเพิ่มคำตอบเพิ่มเติมให้กับ @VonC ที่ยอดเยี่ยมและให้ข้อมูล

ฉันแปลกใจที่ไม่มีใครพูดถึงค่าบำรุงรักษาเมื่อโครงการมีขนาดใหญ่ขึ้นนักพัฒนาเก่าออกไปและคนใหม่มา ไปแน่นอนเป็นภาษาเด็ก

โดยทั่วไปฉันพยายามหลีกเลี่ยงคำชี้เมื่อทำได้ แต่พวกเขามีสถานที่และความสวยงาม

ฉันใช้พอยน์เตอร์เมื่อ:

  • ทำงานกับชุดข้อมูลขนาดใหญ่
  • มีโครงสร้างการรักษาสถานะเช่น TokenCache
    • ฉันตรวจสอบให้แน่ใจว่าฟิลด์ทั้งหมดเป็นแบบส่วนตัวการโต้ตอบสามารถทำได้ผ่านตัวรับวิธีการที่กำหนดเท่านั้น
    • ฉันไม่ส่งฟังก์ชั่นนี้ให้กับ goroutine ใด ๆ

เช่น:

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
}

เหตุผลที่ฉันหลีกเลี่ยงตัวชี้:

  • ตัวชี้ไม่ปลอดภัยในเวลาเดียวกัน (จุดรวมของ GoLang)
  • หนึ่งครั้งตัวรับตัวชี้ตัวรับตัวชี้เสมอ (สำหรับวิธีการของโครงสร้างทั้งหมดเพื่อความสอดคล้อง)
  • mutexes มีราคาแพงกว่าช้ากว่าและดูแลรักษายากกว่าเมื่อเทียบกับ "ค่าสำเนามูลค่า"
  • การพูดถึง "มูลค่าสำเนาต้นทุน" เป็นปัญหาจริงหรือ? การเพิ่มประสิทธิภาพก่อนวัยอันควรเป็นรากฐานของความชั่วร้ายทั้งหมดคุณสามารถเพิ่มคำแนะนำในภายหลังได้ตลอดเวลา
  • โดยตรงบังคับให้ฉันออกแบบโครงสร้างเล็ก ๆ
  • ส่วนใหญ่สามารถหลีกเลี่ยงตัวชี้ได้โดยการออกแบบฟังก์ชั่นบริสุทธิ์โดยมีเจตนาชัดเจนและ I / O ที่ชัดเจน
  • ฉันเชื่อว่าการเก็บขยะทำได้ยากขึ้นด้วยคำแนะนำ
  • ง่ายต่อการโต้แย้งเกี่ยวกับการห่อหุ้มความรับผิดชอบ
  • ทำให้มันง่ายโง่ (ใช่คำแนะนำอาจเป็นเรื่องยากเพราะคุณไม่เคยรู้จัก dev ของโครงการถัดไป)
  • การทดสอบหน่วยก็เหมือนกับการเดินผ่านสวนสีชมพู (สำนวนเฉพาะภาษาสโลวัก?) หมายความว่าง่าย
  • ไม่มี NIL หากเงื่อนไข (สามารถส่งผ่าน NIL ในที่ที่คาดว่าจะมีตัวชี้)

หลักทั่วไปของฉันเขียนวิธีการห่อหุ้มให้ได้มากที่สุดเช่น:

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) 

อัพเดท:

คำถามนี้เป็นแรงบันดาลใจให้ฉันค้นคว้าหัวข้อนี้มากขึ้นและเขียนบล็อกโพสต์เกี่ยวกับเรื่องนี้https://medium.com/gophersland/gopher-vs-object-oriented-golang-4fa62b88c701


ฉันชอบ 99% ของสิ่งที่คุณพูดที่นี่และเห็นด้วยอย่างยิ่งกับมัน นั่นหมายความว่าฉันสงสัยว่าตัวอย่างของคุณเป็นวิธีที่ดีที่สุดในการแสดงประเด็นของคุณหรือไม่ TokenCache ไม่ใช่แผนที่เป็นหลัก (จาก @VonC - "ถ้าผู้รับเป็นแผนที่ func หรือ chan อย่าใช้ตัวชี้ไปที่มัน") เนื่องจากแผนที่เป็นประเภทอ้างอิงคุณได้อะไรจากการทำให้ "Add ()" เป็นตัวรับตัวชี้ สำเนาใด ๆ ของ TokenCache จะอ้างอิงแผนที่เดียวกัน ดู Go playground - play.golang.com/p/Xda1rsGwvhq
รวย

ดีใจที่เราสอดคล้องกัน จุดที่ดี อันที่จริงฉันคิดว่าฉันใช้ตัวชี้ในตัวอย่างนี้เพราะฉันคัดลอกมาจากโปรเจ็กต์ที่ TokenCache จัดการสิ่งต่างๆมากกว่าแผนที่นั้น และถ้าฉันใช้ตัวชี้ในวิธีการเดียวฉันจะใช้ตัวชี้นี้ในทั้งหมด คุณแนะนำให้ลบตัวชี้ออกจากตัวอย่าง SO นี้หรือไม่
Lukas Lukac

ฮ่า ๆ คัดลอก / วางการประท้วงอีกครั้ง! 😉 IMO คุณสามารถปล่อยให้มันเป็นเหมือนเดิมได้เนื่องจากมันแสดงให้เห็นกับกับดักที่ง่ายต่อการตกหรือคุณอาจแทนที่แผนที่ด้วยสิ่งที่แสดงสถานะและ / หรือโครงสร้างข้อมูลขนาดใหญ่
รวย

ฉันแน่ใจว่าพวกเขาจะอ่านความคิดเห็น ... PS: รวยข้อโต้แย้งของคุณดูสมเหตุสมผลเพิ่มฉันใน LinkedIn (ลิงก์ในโปรไฟล์ของฉัน) ยินดีที่จะเชื่อมต่อ
Lukas Lukac
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.