เหตุใดพารามิเตอร์ const จึงไม่อนุญาตให้ใช้ใน C #


102

มันดูแปลกโดยเฉพาะสำหรับนักพัฒนา C ++ ใน C ++ เราใช้เพื่อทำเครื่องหมายพารามิเตอร์เป็นconstเพื่อให้แน่ใจว่าสถานะจะไม่เปลี่ยนแปลงในวิธีการ นอกจากนี้ยังมีเหตุผลเฉพาะอื่น ๆ ของ C ++ เช่นการส่งผ่านconst refเพื่อส่งต่อโดยอ้างอิงและต้องแน่ใจว่าสถานะจะไม่เปลี่ยนแปลง แต่ทำไมเราไม่สามารถทำเครื่องหมายเป็นพารามิเตอร์วิธี const ใน C # ได้?

เหตุใดฉันจึงประกาศวิธีการดังต่อไปนี้ไม่ได้

    ....
    static void TestMethod1(const MyClass val)
    {}
    ....
    static void TestMethod2(const int val)
    {}
    ....

มันเป็นเพียงฟีเจอร์จับความปลอดภัยใน C ++ เสมอและไม่เคยเป็นส่วนสำคัญกับภาษาอย่างที่ฉันเห็น C # devs ไม่คิดว่ามันเพิ่มมูลค่าได้มากอย่างเห็นได้ชัด
Noldorin

3
การอภิปรายที่ใหญ่กว่าสำหรับคำถามนี้: "Const-correctness ใน c #": stackoverflow.com/questions/114149/const-correctness-in-c
tenpn

มีสำหรับประเภทค่า [out / ref] แต่ไม่ใช่ประเภทอ้างอิง เป็นคำถามที่น่าสนใจ
kenny

4
คำถามนี้มีทั้งแท็ก C # และภาษาที่ไม่เข้าใจได้อย่างไร
CJ7

1
ข้อมูลและฟังก์ชันที่ไม่เปลี่ยนรูปโดยไม่มีผลข้างเคียงนั้นง่ายกว่าที่จะให้เหตุผล คุณอาจจะสนุกกับ F # fsharpforfunandprofit.com/posts/is-your-language-unreasonable
Colonel Panic

คำตอบ:


59

นอกจากคำตอบที่ดีอื่น ๆ แล้วฉันจะเพิ่มเหตุผลอีกประการหนึ่งว่าทำไมไม่ใส่ C-style constness ลงใน C # คุณพูดว่า:

เราทำเครื่องหมายพารามิเตอร์เป็น const เพื่อให้แน่ใจว่าสถานะจะไม่ถูกเปลี่ยนในวิธีการ

ถ้า const ทำอย่างนั้นจริงจะดีมาก Const ไม่ทำอย่างนั้น const เป็นเรื่องโกหก!

Const ไม่รับประกันว่าฉันสามารถใช้งานได้จริง สมมติว่าคุณมีวิธีการที่ใช้สิ่งที่เป็น const มีผู้เขียนโค้ดสองคน: คนเขียนผู้โทรและคนที่เขียนไฟล์ callee ผู้เขียน callee ได้ทำให้เมธอดใช้ const ผู้เขียนทั้งสองคนคิดว่าอะไรไม่แน่นอนเกี่ยวกับวัตถุ?

ไม่มีอะไร callee มีอิสระที่จะทิ้ง const และทำให้วัตถุกลายพันธุ์ดังนั้นผู้เรียกจึงไม่รับประกันว่าการเรียกเมธอดที่ใช้ const จริงจะไม่กลายพันธุ์ ในทำนองเดียวกัน callee ไม่สามารถสันนิษฐานได้ว่าเนื้อหาของวัตถุจะไม่เปลี่ยนแปลงตลอดการกระทำของ callee; callee สามารถเรียกวิธีการกลายพันธุ์บางอย่างบนนามแฝงที่ไม่ใช่ constของวัตถุ const และตอนนี้วัตถุที่เรียกว่า const มีเปลี่ยนไปแล้ว

C-style const ไม่รับประกันว่าวัตถุจะไม่เปลี่ยนแปลงดังนั้นจึงเสีย ตอนนี้ C มีระบบประเภทที่อ่อนแออยู่แล้วซึ่งคุณสามารถตีความการร่ายซ้ำสองครั้งเป็น int ได้ถ้าคุณต้องการจริงๆดังนั้นจึงไม่ควรแปลกใจที่มันมีระบบประเภทที่อ่อนแอเมื่อเทียบกับ const เช่นกัน แต่ C # ได้รับการออกแบบให้มีระบบประเภทที่ดีซึ่งเป็นระบบประเภทที่เมื่อคุณพูดว่า "ตัวแปรนี้มีสตริง" แสดงว่าตัวแปรมีการอ้างอิงถึงสตริง (หรือ null) จริงๆ แน่นอนว่าเราไม่ต้องการที่จะใส่ C-สไตล์ "const" ปรับปรุงให้เป็นระบบการพิมพ์เพราะเราไม่ต้องการระบบการพิมพ์ที่จะโกหก เราต้องการให้ระบบประเภทมีความแข็งแกร่งเพื่อให้คุณสามารถหาเหตุผลได้อย่างถูกต้องเกี่ยวกับรหัสของคุณ

Const ใน C เป็นแนวทาง ; โดยพื้นฐานแล้วหมายความว่า "คุณสามารถเชื่อใจฉันได้ว่าจะไม่พยายามทำให้สิ่งนี้กลายพันธุ์" ที่ไม่ควรอยู่ในระบบ type ; สิ่งต่างๆในระบบ typeควรเป็นข้อเท็จจริงเกี่ยวกับออบเจ็กต์ที่คุณสามารถให้เหตุผลได้ไม่ใช่แนวทางในการใช้งาน

ตอนนี้อย่าเข้าใจฉันผิด เพียงเพราะ const ใน C แตกลึกไม่ได้หมายความว่าแนวคิดทั้งหมดไม่มีประโยชน์ สิ่งที่ฉันอยากเห็นคือสิ่งที่ถูกต้องและมีประโยชน์จริงๆรูปแบบคำอธิบายประกอบ "const" ที่ใน C # ซึ่งเป็นคำอธิบายประกอบที่ทั้งมนุษย์และคอมไพเลอร์สามารถใช้เพื่อช่วยให้พวกเขาเข้าใจโค้ดและรันไทม์สามารถใช้ทำสิ่งต่างๆเช่นการทำให้พาราเลลไลเซชันอัตโนมัติและ การเพิ่มประสิทธิภาพขั้นสูงอื่น ๆ

ตัวอย่างเช่นลองนึกภาพว่าคุณสามารถ "วาดกล่อง" รอบ ๆ โค้ดและพูดว่า "ฉันรับประกันว่าโค้ดส่วนใหญ่นี้จะไม่มีการกลายพันธุ์ในฟิลด์ใด ๆ ของคลาสนี้" ในแบบที่คอมไพเลอร์สามารถตรวจสอบได้ หรือวาดกรอบที่ระบุว่า " วิธีการที่บริสุทธิ์นี้ทำให้สถานะภายในของวัตถุกลายพันธุ์ แต่ไม่ใช่ด้วยวิธีใด ๆ ที่สังเกตได้นอกกรอบ" วัตถุดังกล่าวอาจจะไม่ปลอดภัยแบบมัลติเธรดโดยอัตโนมัติ แต่มันอาจจะ memoized มีคำอธิบายประกอบที่น่าสนใจทุกประเภทที่เราสามารถใส่รหัสที่จะช่วยให้สามารถเพิ่มประสิทธิภาพที่สมบูรณ์และความเข้าใจที่ลึกซึ้งยิ่งขึ้น เราสามารถทำได้ดีกว่าคำอธิบายประกอบ const สไตล์ C ที่อ่อนแอ

แต่ผมเน้นว่านี้เป็นเพียงการเก็งกำไร เราไม่มีแผนที่จะนำคุณลักษณะประเภทนี้ไปใช้ใน C # เวอร์ชันสมมุติในอนาคตหากยังมีอยู่ซึ่งเรายังไม่ได้ประกาศไม่ทางใดก็ทางหนึ่ง เป็นสิ่งที่ฉันชอบที่จะเห็นและบางสิ่งบางอย่างที่การให้ความสำคัญกับการประมวลผลแบบมัลติคอร์อาจจำเป็นต้องใช้ แต่สิ่งเหล่านี้ไม่ควรตีความว่าเป็นการคาดคะเนหรือการรับประกันคุณลักษณะเฉพาะหรือทิศทางในอนาคตสำหรับ C #

ตอนนี้หากสิ่งที่คุณต้องการเป็นเพียงคำอธิบายประกอบบนตัวแปรภายในซึ่งเป็นพารามิเตอร์ที่ระบุว่า "ค่าของพารามิเตอร์นี้ไม่เปลี่ยนแปลงตลอดทั้งวิธีการ" แน่นอนว่าจะทำได้อย่างง่ายดาย เราสามารถรองรับภาษาท้องถิ่นและพารามิเตอร์ "แบบอ่านอย่างเดียว" ที่จะเริ่มต้นครั้งเดียวและข้อผิดพลาดเวลาคอมไพล์เพื่อเปลี่ยนวิธีการ ตัวแปรที่ประกาศโดยคำสั่ง "ใช้" นั้นเป็นตัวแปรเฉพาะที่อยู่แล้ว เราสามารถเพิ่มคำอธิบายประกอบที่เป็นทางเลือกให้กับคนในพื้นที่และพารามิเตอร์ทั้งหมดเพื่อให้ทำหน้าที่เหมือนตัวแปร "ใช้" ไม่เคยเป็นคุณลักษณะที่มีลำดับความสำคัญสูงมากดังนั้นจึงไม่เคยมีการนำมาใช้


34
ขออภัยฉันพบว่าข้อโต้แย้งนี้อ่อนแอมากความจริงที่ว่านักพัฒนาสามารถพูดคำโกหกไม่สามารถป้องกันได้ในทุกภาษา เป็นโมฆะ foo (const A & a) ที่ปรับเปลี่ยนวัตถุ a ไม่แตกต่างจาก int countApples (Basket b) ที่ส่งคืนจำนวนลูกแพร์ มันเป็นเพียงข้อผิดพลาดข้อบกพร่องที่รวบรวมในภาษาใดก็ได้
Alessandro Teruzzi

55
“ คาลลีมีอิสระที่จะทิ้ง const …”เอ่อ… (° _ °) นี่เป็นข้อโต้แย้งที่ค่อนข้างตื้นที่คุณทำที่นี่ 0xDEADBEEFผู้ถูกเรียกยังมีอิสระที่จะเพียงแค่เริ่มต้นการเขียนตำแหน่งหน่วยความจำแบบสุ่มด้วย แต่ทั้งสองจะเป็นการเขียนโปรแกรมที่แย่มากและถูกคอมไพเลอร์สมัยใหม่ใด ๆ จับได้เร็วและรุนแรง ในอนาคตเมื่อเขียนคำตอบโปรดอย่าสับสนว่าอะไรเป็นไปได้ในทางเทคนิคโดยใช้แบ็คดอร์ของภาษากับสิ่งที่เป็นไปได้ในโค้ดจริงในโลกแห่งความเป็นจริง ข้อมูลที่บิดเบือนเช่นนี้จะสร้างความสับสนให้กับผู้อ่านที่มีประสบการณ์น้อยและทำให้คุณภาพของข้อมูลใน SO ลดลง
Slipp D.Thompson

8
“ Const in C เป็นแนวทาง;” การอ้างแหล่งที่มาของบางสิ่งที่คุณพูดจะช่วยคุณได้มาก ในประสบการณ์การศึกษาและอุตสาหกรรมของฉันข้อความที่ยกมาคือ hogwash - const ใน C เป็นคุณสมบัติที่จำเป็นในตัวมากพอ ๆ กับระบบประเภทเอง - ทั้งสองมีประตูหลังเพื่อโกงเมื่อจำเป็น (เช่นเดียวกับทุกอย่างใน C) แต่คาดว่า หนังสือจะใช้ใน 99.99% ของรหัส
Slipp D.Thompson

29
คำตอบนี้คือสาเหตุที่บางครั้งฉันเกลียดการออกแบบ C # นักออกแบบภาษามั่นใจอย่างยิ่งว่าพวกเขาฉลาดกว่านักพัฒนาคนอื่น ๆ มากและพยายามปกป้องพวกเขาจากข้อบกพร่องมากเกินไป เห็นได้ชัดว่าคำหลัก const ใช้งานได้เฉพาะในเวลาคอมไพล์เท่านั้นเนื่องจากใน C ++ เราสามารถเข้าถึงหน่วยความจำได้โดยตรงและสามารถทำอะไรก็ได้ที่เราต้องการและมีหลายวิธีในการรับการประกาศ const รอบ แต่ไม่มีใครทำเช่นนี้ในสถานการณ์ปกติ อย่างไรก็ตาม 'ส่วนตัว' และ 'ป้องกัน' ใน C # ก็ไม่รับประกันเช่นกันเนื่องจากเราสามารถเข้าถึงวิธีการหรือฟิลด์เหล่านี้โดยใช้การสะท้อนกลับ
BlackOverlord

13
@BlackOverlord: (1) ความปรารถนาที่จะออกแบบภาษาที่คุณสมบัติโดยธรรมชาติจะนำนักพัฒนาไปสู่ความสำเร็จมากกว่าความล้มเหลวนั้นได้รับแรงบันดาลใจจากความปรารถนาที่จะสร้างเครื่องมือที่มีประโยชน์ไม่ใช่ด้วยความเชื่อที่ว่านักออกแบบฉลาดกว่าลูกค้าอย่างที่คุณคาดเดา (2) การสะท้อนกลับไม่ใช่ส่วนหนึ่งของภาษา C # เป็นส่วนหนึ่งของรันไทม์ การเข้าถึงการสะท้อนถูก จำกัด โดยระบบรักษาความปลอดภัยของรันไทม์ รหัสความน่าเชื่อถือต่ำไม่ได้รับความสามารถในการสะท้อนความเป็นส่วนตัว ค้ำประกันให้โดยการปรับเปลี่ยนการเข้าถึงสำหรับรหัสความไว้วางใจต่ำ ; รหัสความน่าเชื่อถือสูงสามารถทำอะไรก็ได้
Eric Lippert

24

สาเหตุหนึ่งที่ไม่มีความถูกต้องใน C # เนื่องจากไม่มีอยู่ในระดับรันไทม์ โปรดจำไว้ว่า C # 1.0 ไม่มีคุณสมบัติใด ๆ เว้นแต่จะเป็นส่วนหนึ่งของรันไทม์

และสาเหตุหลายประการที่ CLR ไม่มีแนวคิดเรื่องความถูกต้องของ const ตัวอย่างเช่น:

  1. มันทำให้รันไทม์ซับซ้อน นอกจากนี้ JVM ก็ไม่มีเช่นกันและโดยพื้นฐานแล้ว CLR เริ่มต้นเป็นโปรเจ็กต์เพื่อสร้างรันไทม์ที่เหมือน JVM ไม่ใช่รันไทม์ C ++ เหมือน
  2. หากมีความถูกต้องของ const ในระดับรันไทม์ควรมีความถูกต้องของ const ใน BCL มิฉะนั้นคุณลักษณะนี้จะไม่มีจุดหมายมากเท่าที่. NET Framework เกี่ยวข้อง
  3. แต่ถ้า BCL ต้องการความถูกต้อง const ทุกภาษาที่อยู่ด้านบนของ CLR ควรรองรับความถูกต้องของ const (VB, JavaScript, Python, Ruby, F # ฯลฯ ) นั่นจะไม่เกิดขึ้น

ความถูกต้องของ Const เป็นคุณลักษณะของภาษาที่มีอยู่ใน C ++ เท่านั้น ดังนั้นจึงค่อนข้างขัดแย้งกับข้อโต้แย้งเดียวกันว่าเหตุใด CLR จึงไม่ต้องการข้อยกเว้นที่ตรวจสอบแล้ว (ซึ่งเป็นคุณลักษณะเฉพาะภาษา Java)

นอกจากนี้ฉันไม่คิดว่าคุณสามารถแนะนำคุณลักษณะระบบพื้นฐานประเภทนี้ในสภาพแวดล้อมที่มีการจัดการโดยไม่ทำลายความเข้ากันได้แบบย้อนหลัง ดังนั้นอย่านับความถูกต้องของ const ที่เคยเข้าสู่โลก C #


แน่ใจหรือว่าไม่มี JVM อย่างที่รู้ ๆ กันก็มีนะ คุณสามารถลอง TestMethod โมฆะแบบคงที่สาธารณะ (int val สุดท้าย) ใน Java
ไม่ระบุตัวตน

2
Final ไม่ใช่ const จริงๆ คุณยังคงสามารถเปลี่ยนฟิลด์ใน IIRC อ้างอิงได้ แต่ไม่ใช่ค่า / การอ้างอิงเท่านั้น (ซึ่งจะถูกส่งผ่านด้วยค่าดังนั้นจึงเป็นเคล็ดลับของคอมไพเลอร์มากกว่าที่จะป้องกันไม่ให้คุณเปลี่ยนค่าพารามิเตอร์)
Ruben

1
สัญลักษณ์แสดงหัวข้อย่อยที่ 3 ของคุณเป็นความจริงเพียงบางส่วนเท่านั้น การมีพารามิเตอร์ const สำหรับการเรียก BCL จะไม่เป็นการเปลี่ยนแปลงที่ผิดปกติเนื่องจากเป็นเพียงการอธิบายสัญญาที่แม่นยำยิ่งขึ้น "วิธีนี้จะไม่เปลี่ยนสถานะของวัตถุ" ตรงกันข้ามกับ "วิธีนี้ต้องการให้คุณส่งค่า const" ซึ่งจะทำลาย
Rune FS

2
มีการบังคับใช้ภายในวิธีการดังนั้นจึงไม่เป็นการเปลี่ยนแปลงที่ผิดปกติที่จะแนะนำใน BCL
รูน FS

1
แม้ว่ารันไทม์จะไม่สามารถบังคับใช้ได้ แต่การมีแอ็ตทริบิวต์ที่ระบุว่าควรอนุญาต / คาดว่าจะเขียนฟังก์ชันไปยังrefพารามิเตอร์ (โดยเฉพาะในกรณีของเมธอด / คุณสมบัติของโครงสร้างthis) หากเมธอดระบุว่าจะไม่เขียนไปยังrefพารามิเตอร์คอมไพเลอร์ควรอนุญาตให้ส่งค่าแบบอ่านอย่างเดียว ในทางกลับกันหากเมธอดระบุว่าจะเขียนthisคอมไพเลอร์ไม่ควรอนุญาตให้เรียกใช้เมธอดดังกล่าวกับค่าแบบอ่านอย่างเดียว
supercat

12

ฉันเชื่อว่ามีสองเหตุผลที่ C # ไม่ถูกต้อง

ที่แรกก็คือunderstandibility โปรแกรมเมอร์ C ++ ไม่กี่คนที่เข้าใจความถูกต้องของ const ตัวอย่างง่ายๆของconst int argน่ารัก แต่ฉันก็เคยเห็นเช่นchar * const * const argตัวชี้ค่าคงที่ไปยังตัวชี้ค่าคงที่สำหรับอักขระที่ไม่คงที่ ความถูกต้องคงที่ของตัวชี้ไปยังฟังก์ชันเป็นระดับใหม่ของการทำให้สับสน

ประการที่สองคือเนื่องจากอาร์กิวเมนต์คลาสเป็นการอ้างอิงที่ส่งผ่านค่า ซึ่งหมายความว่ามีอยู่แล้วสองระดับของ constness ที่จะจัดการกับอย่างเห็นได้ชัดโดยไม่ต้องชัดเจนไวยากรณ์ จุดสะดุดที่คล้ายกันคือคอลเลกชัน (และคอลเลกชันของคอลเลกชัน ฯลฯ )

ความถูกต้องของค่าคงที่เป็นส่วนสำคัญของระบบประเภท C ++ ในทางทฤษฎี - สามารถเพิ่ม C # เป็นสิ่งที่ตรวจสอบในเวลาคอมไพล์เท่านั้น (ไม่จำเป็นต้องเพิ่มลงใน CLR และจะไม่ส่งผลกระทบต่อ BCL เว้นแต่จะรวมความคิดของเมธอดสมาชิก const ไว้ด้วย) .

อย่างไรก็ตามฉันเชื่อว่าสิ่งนี้ไม่น่าจะเป็นไปได้: เหตุผลที่สอง (ไวยากรณ์) นั้นค่อนข้างยากที่จะแก้ไขซึ่งจะทำให้เหตุผลแรก (ความเข้าใจได้) เป็นปัญหามากยิ่งขึ้น


1
คุณคิดถูกที่ CLR ไม่จำเป็นต้องกังวลเกี่ยวกับเรื่องนี้เนื่องจากสามารถใช้modoptและmodreqในระดับข้อมูลเมตาเพื่อจัดเก็บข้อมูลนั้นได้ (ตามที่ C + / CLI ทำไปแล้ว - ดูSystem.Runtime.CompilerServices.IsConst) และสิ่งนี้สามารถขยายไปสู่วิธีการ const ได้เล็กน้อยเช่นกัน
Pavel Minaev

คุ้มค่า แต่ต้องทำในลักษณะที่ปลอดภัย ความลึก / ตื้นที่คุณพูดถึงจะเปิดเวิร์มทั้งกระป๋อง
user1496062

ใน c ++ ที่มีการจัดการการอ้างอิงจริงฉันเห็นข้อโต้แย้งของคุณเกี่ยวกับการมี const สองประเภท อย่างไรก็ตามใน C # (ซึ่งคุณต้องผ่านประตูหลังเพื่อใช้ "พอยน์เตอร์") ดูเหมือนจะชัดเจนเพียงพอสำหรับฉันว่ามีความหมายที่ชัดเจนสำหรับประเภทการอ้างอิง (เช่นคลาส) และมีการกำหนดไว้อย่างชัดเจนแม้ว่าจะมีความหมายที่แตกต่างกันสำหรับค่า ประเภท (เช่น primitives, structs)
Assimilater

2
โปรแกรมเมอร์ที่ไม่เข้าใจ const-ness ไม่ควรเขียนโปรแกรมใน C ++
Steve White

5

constหมายถึง "ค่าคงที่เวลาคอมไพล์" ใน C # ไม่ใช่ "อ่านอย่างเดียว แต่รหัสอื่นอาจเปลี่ยนแปลงได้" เช่นเดียวกับใน C ++ อะนาล็อกคร่าวๆของ C ++ constใน C # คือreadonlyแต่อันนั้นใช้ได้กับฟิลด์เท่านั้น นอกเหนือจากนั้นไม่มี C ++ เหมือนแนวคิดเรื่องความถูกต้อง const ใน C # เลย

เหตุผลนั้นยากที่จะบอกได้อย่างแน่นอนเพราะมีหลายเหตุผลที่เป็นไปได้ แต่การคาดเดาของฉันคือต้องการให้ภาษาเรียบง่ายเป็นอันดับแรกและสำคัญที่สุดและมีข้อดีที่ไม่แน่นอนของการนำไปใช้ในวินาทีนี้


ดังนั้นคุณคิดว่า MS ถือว่า 'noise' มีพารามิเตอร์ const ในวิธีการ
ไม่ระบุตัวตน

1
ฉันไม่แน่ใจว่า "เสียงรบกวน" คืออะไรขอเรียกว่า "อัตราส่วนต้นทุน / ผลประโยชน์สูง" แต่แน่นอนว่าคุณจะต้องมีคนจากทีม C # มาบอกคุณอย่างแน่นอน
Pavel Minaev

ดังนั้นเมื่อฉันพูดถึงมันข้อโต้แย้งของคุณก็คือความมั่นคงถูกละไว้จาก C # เพื่อให้มันง่าย ลองคิดดูสิ
Steve White

2

เป็นไปได้ตั้งแต่ C # 7.2 (ธันวาคม 2017 ด้วย Visual Studio 2017 15.5)

สำหรับโครงสร้างและประเภทพื้นฐานเท่านั้น! ไม่ใช่สำหรับสมาชิกชั้นเรียน

คุณต้องใช้inเพื่อส่งอาร์กิวเมนต์เป็นอินพุตโดยการอ้างอิง ดู:

ดู: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/in-parameter-modifier

สำหรับตัวอย่างของคุณ:

....
static void TestMethod1(in MyStruct val)
{
    val = new MyStruct;  // Error CS8331 Cannot assign to variable 'in MyStruct' because it is a readonly variable
    val.member = 0;  // Error CS8332 Cannot assign to a member of variable 'MyStruct' because it is a readonly variable
}
....
static void TestMethod2(in int val)
{
    val = 0;  // Error CS8331 Cannot assign to variable 'in int' because it is a readonly variable
}
....
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.