Java generics ประเภทลบ: เมื่อใดและจะเกิดอะไรขึ้น


238

ฉันอ่านเกี่ยวกับการลบประเภทของ Java บนเว็บไซต์ของออราเคิล

การลบประเภทเกิดขึ้นเมื่อใด ในเวลารวบรวมหรือรันไทม์? เมื่อชั้นเรียนถูกโหลด? เมื่อชั้นเรียนถูกยกตัวอย่าง?

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

ลองพิจารณาตัวอย่างต่อไปนี้: ระดับ Say มีวิธีการที่A empty(Box<? extends Number> b)เรารวบรวมและรับไฟล์ชั้นเรียนA.javaA.class

public class A {
    public static void empty(Box<? extends Number> b) {}
}
public class Box<T> {}

ตอนนี้เราสร้างชั้นอื่นBซึ่งเรียกวิธีการที่emptyมีข้อโต้แย้งที่ไม่แปร empty(new Box())(ชนิดดิบ): ถ้าเรารวบรวมB.javaกับA.classใน classpath ที่ javac เป็นพอสมาร์ทที่จะยกระดับการเตือน ดังนั้นจึงA.class มีข้อมูลประเภทที่เก็บอยู่ในนั้น

public class B {
    public static void invoke() {
        // java: unchecked method invocation:
        //  method empty in class A is applied to given types
        //  required: Box<? extends java.lang.Number>
        //  found:    Box
        // java: unchecked conversion
        //  required: Box<? extends java.lang.Number>
        //  found:    Box
        A.empty(new Box());
    }
}

ฉันเดาว่าการลบประเภทนั้นจะเกิดขึ้นเมื่อโหลดคลาส แต่เป็นเพียงการเดา แล้วจะเกิดอะไรขึ้น


2
คำถามเพิ่มเติมรุ่น "ทั่วไป" นี้: stackoverflow.com/questions/313584/ …
Ciro Santilli 法轮功冠状病病六四事件法轮功

@ กระทะทอด: บทความที่กล่าวถึงในคำตอบของฉันอธิบายในรายละเอียดว่าจะลบประเภทอย่างไรและอย่างไร นอกจากนี้ยังอธิบายเมื่อเก็บข้อมูลประเภท กล่าวอีกนัยหนึ่ง: ข้อมูลทั่วไป reified มีอยู่ใน Java ตรงกันข้ามกับความเชื่อที่แพร่หลาย ดู: rgomes.info/using-typetokens-to-retrieve-generic-parameters
Richard Gomes

คำตอบ:


240

การลบประเภทนำไปใช้กับการใช้งานของ generics มีเมทาดาทาแน่นอนในไฟล์คลาสที่จะบอกว่าเมธอด / ประเภทนั้นเป็นแบบทั่วไปหรือไม่และมีข้อ จำกัด อะไรบ้าง แต่เมื่อมีการใช้ข้อมูลทั่วไปพวกมันจะถูกแปลงเป็นการตรวจสอบเวลาแบบคอมไพล์และเวลาดำเนินการ ดังนั้นรหัสนี้:

List<String> list = new ArrayList<String>();
list.add("Hi");
String x = list.get(0);

ถูกรวบรวมเป็น

List list = new ArrayList();
list.add("Hi");
String x = (String) list.get(0);

ณ เวลาที่ทำการดำเนินการไม่มีวิธีการค้นหาว่าT=Stringสำหรับวัตถุรายการ - ข้อมูลนั้นหายไป

... แต่List<T>อินเทอร์เฟซยังคงโฆษณาตัวเองว่าเป็นเรื่องทั่วไป

แก้ไข: เพื่อชี้แจง, คอมไพเลอร์จะเก็บข้อมูลเกี่ยวกับตัวแปรที่เป็นList<String>- แต่คุณยังไม่สามารถหาสิ่งนั้นT=Stringสำหรับรายการวัตถุเอง


6
ไม่แม้ในการใช้งานประเภททั่วไปอาจมีเมทาดาทาอยู่ในขณะใช้งานจริง ตัวแปรท้องถิ่นไม่สามารถเข้าถึงได้ผ่าน Reflection แต่สำหรับพารามิเตอร์เมธอดที่ประกาศเป็น "List <String> l" จะมีข้อมูลเมตาประเภทที่รันไทม์พร้อมใช้งานผ่าน Reflection API อ๋อ "ลบออกประเภท" ไม่ได้เป็นง่ายๆเป็นคนจำนวนมากคิดว่า ...
Rogério

4
@Rogerio: ขณะที่ผมตอบกลับความคิดเห็นของคุณผมเชื่อว่าคุณกำลังการสับสนระหว่างความสามารถในการได้รับชนิดของที่ตัวแปรและความสามารถที่จะได้รับชนิดของนั้นวัตถุ วัตถุเองไม่ทราบชนิดอาร์กิวเมนต์แม้ว่าเขตข้อมูลจะเป็นเช่นนั้น
Jon Skeet

แน่นอนเพียงแค่มองวัตถุเองคุณก็ไม่รู้ว่ามันเป็น List <String> แต่วัตถุจะไม่ปรากฏขึ้นจากที่ไหนเลย พวกมันถูกสร้างขึ้นแบบโลคอลส่งผ่านเป็นอาร์กิวเมนต์การเรียกใช้เมธอดส่งคืนเป็นค่าส่งคืนจากการเรียกใช้เมธอดหรืออ่านจากฟิลด์ของวัตถุบางอย่าง ... ในทุกกรณีคุณสามารถรู้ได้ในขณะทำงานว่าประเภททั่วไปคืออะไร โดยนัยหรือโดยใช้ Java Reflection API
Rogério

13
@Rogerio: คุณรู้ได้อย่างไรว่าวัตถุมาจากไหน? หากคุณมีพารามิเตอร์ประเภทList<? extends InputStream>คุณจะรู้ได้อย่างไรว่ามันเป็นประเภทใดเมื่อมันถูกสร้างขึ้น? แม้ว่าคุณจะสามารถค้นหาประเภทของเขตข้อมูลที่เก็บไว้ในการอ้างอิงทำไมคุณต้อง? เหตุใดคุณจึงควรได้รับข้อมูลส่วนที่เหลือทั้งหมดเกี่ยวกับวัตถุ ณ เวลาดำเนินการ แต่ไม่ใช่อาร์กิวเมนต์ประเภททั่วไป ดูเหมือนว่าคุณกำลังพยายามทำให้การลบประเภทเป็นสิ่งเล็ก ๆ น้อย ๆ ซึ่งไม่ส่งผลกระทบต่อนักพัฒนาจริงๆ - ในขณะที่ฉันพบว่ามันเป็นปัญหาที่สำคัญมากในบางกรณี
Jon Skeet

แต่การลบประเภทเป็นสิ่งเล็ก ๆ ที่ไม่ส่งผลกระทบต่อผู้พัฒนาจริงๆ! แน่นอนฉันไม่สามารถพูดกับคนอื่นได้ แต่จากประสบการณ์ของฉันมันไม่เคยเป็นเรื่องใหญ่อะไร ฉันใช้ประโยชน์จากข้อมูลประเภทรันไทม์ในการออกแบบ Java mocking API (JMockit) ของฉัน แดกดัน. NET mocking APIs ดูเหมือนจะใช้ประโยชน์จากระบบประเภททั่วไปที่มีอยู่ใน C # น้อยลง
Rogério

99

คอมไพเลอร์มีหน้าที่ทำความเข้าใจ Generics ในเวลารวบรวม คอมไพเลอร์ยังเป็นผู้รับผิดชอบสำหรับการขว้างปาออกไปนี้ "ความเข้าใจ" ของชั้นเรียนทั่วไปในกระบวนการที่เราเรียกว่าลบออกประเภท ทั้งหมดเกิดขึ้นในเวลารวบรวม

หมายเหตุ:ตรงกันข้ามกับความเชื่อของนักพัฒนา Java ส่วนใหญ่เป็นไปได้ที่จะเก็บข้อมูลประเภทเวลาคอมไพล์และดึงข้อมูลนี้ในขณะใช้งานจริงแม้จะมีวิธีที่ จำกัด มาก ในคำอื่น ๆ : Java ไม่ให้ยาชื่อสามัญ reified ในทางที่

เกี่ยวกับการลบประเภท

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

เกี่ยวกับการปรับเปลี่ยนข้อมูลทั่วไปใน Java

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

ฉันเขียนบทความเกี่ยวกับเรื่องนี้:

https://rgomes.info/using-typetokens-to-retrieve-generic-parameters/

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

โค้ดตัวอย่าง

บทความข้างต้นมีลิงค์ไปยังโค้ดตัวอย่าง


1
@ will824: ฉันได้ปรับปรุงคำตอบให้ดีขึ้นอย่างมากและฉันได้เพิ่มลิงก์ไปยังกรณีทดสอบบางกรณี ไชโย :)
Richard Gomes

1
ที่จริงแล้วพวกเขาไม่ได้รักษาทั้งความเข้ากันได้ของไบนารีและแหล่งที่มา: oracle.com/technetwork/java/javase/compatibility-137462.htmlฉันจะอ่านเพิ่มเติมเกี่ยวกับความตั้งใจของพวกเขาได้ที่ไหน เอกสารบอกว่าใช้การลบประเภท แต่ไม่ได้บอกว่าทำไม
Dzmitry Lazerka

@ Richard แน่นอนบทความที่ยอดเยี่ยม! คุณสามารถเพิ่มว่าคลาสโลคัลทำงานด้วยและในทั้งสองกรณี (คลาสที่ไม่ระบุชื่อและคลาสท้องถิ่น) ข้อมูลเกี่ยวกับอาร์กิวเมนต์ชนิดที่ต้องการจะถูกเก็บไว้เฉพาะในกรณีที่การเข้าถึงโดยตรงnew Box<String>() {};ไม่ใช่ในกรณีของการเข้าถึงทางอ้อมvoid foo(T) {...new Box<T>() {};...}เพราะคอมไพเลอร์ไม่เก็บข้อมูลประเภทสำหรับ การประกาศวิธีการปิดล้อม
Yann-GaëlGuéhéneuc

ฉันได้แก้ไขลิงก์ที่เสียหายไปยังบทความของฉันแล้ว ฉันค่อยๆกำจัดชีวิตของฉันและเรียกคืนข้อมูลของฉัน :-)
Richard Gomes

33

หากคุณมีเขตข้อมูลที่เป็นประเภททั่วไปพารามิเตอร์ประเภทของมันจะถูกรวบรวมลงในชั้นเรียน

หากคุณมีวิธีที่ใช้หรือคืนค่าชนิดทั่วไปพารามิเตอร์ชนิดเหล่านั้นจะถูกคอมไพล์ลงในคลาส

ข้อมูลนี้เป็นสิ่งที่คอมไพเลอร์ใช้เพื่อบอกคุณว่าคุณไม่สามารถส่งผ่านBox<String>ไปยังempty(Box<T extends Number>)วิธีการได้

API ที่มีความซับซ้อน แต่คุณสามารถตรวจสอบข้อมูลข่าวสารประเภทนี้ผ่าน API สะท้อนด้วยวิธีการเช่นgetGenericParameterTypes, และสำหรับสาขาgetGenericReturnTypegetGenericType

หากคุณมีรหัสที่ใช้ประเภททั่วไปคอมไพเลอร์แทรกได้ปลดเปลื้องตามที่จำเป็น (ในโทร) เพื่อตรวจสอบประเภท วัตถุทั่วไปตัวเองเป็นเพียงประเภทดิบ ประเภทพารามิเตอร์คือ "ลบ" ดังนั้นเมื่อคุณสร้าง a new Box<Integer>()ไม่มีข้อมูลเกี่ยวกับIntegerคลาสในBoxวัตถุ

Angelika Langer คำถามที่พบบ่อยคือการอ้างอิงที่ดีที่สุดที่ฉันเคยเห็นสำหรับ Java Generics


2
ที่จริงแล้วมันเป็นประเภทสามัญอย่างเป็นทางการของเขตข้อมูลและวิธีการที่รวบรวมไว้ในชั้นเรียนเช่น typicall "T" ที่จะได้รับจริงชนิดของประเภททั่วไป, คุณต้องใช้"ที่ไม่ระบุชื่อชั้นเคล็ดลับ"
Yann-GaëlGuéhéneuc

13

Generics ใน Java Languageเป็นแนวทางที่ดีจริงๆในหัวข้อนี้

Generics ถูกนำมาใช้โดยคอมไพเลอร์ Java เป็นการแปลงส่วนหน้าเรียกว่าการลบ คุณสามารถ (เกือบ) คิดว่าเป็นการแปลจากแหล่งสู่แหล่งที่มาโดยที่เวอร์ชันทั่วไปของloophole()จะถูกแปลงเป็นเวอร์ชันที่ไม่ใช่ทั่วไป

ดังนั้นจึงเป็นเวลารวบรวม JVM จะไม่มีทางรู้ว่าArrayListคุณใช้อะไร

ฉันอยากจะแนะนำคำตอบของ Mr. Skeet เกี่ยวกับแนวคิดเรื่องการลบข้อมูลทั่วไปใน Java คืออะไร?


6

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

Box<String> b = new Box<String>();
String x = b.getDefault();

ถูกแปลงเป็น

Box b = new Box();
String x = (String) b.getDefault();

ในเวลารวบรวม คุณอาจได้รับคำเตือนไม่ใช่เพราะคอมไพเลอร์รู้ว่าประเภททั่วไปคืออะไร แต่ตรงกันข้ามเพราะมันไม่รู้จักเพียงพอดังนั้นจึงไม่สามารถรับประกันความปลอดภัยประเภท

นอกจากนี้คอมไพเลอร์จะเก็บข้อมูลประเภทเกี่ยวกับพารามิเตอร์ในการเรียกเมธอดซึ่งคุณสามารถดึงข้อมูลผ่านการสะท้อนกลับได้

คู่มือนี้เป็นสิ่งที่ดีที่สุดที่ฉันเคยพบในหัวข้อนี้


6

คำว่า "การลบประเภท" ไม่ใช่คำอธิบายที่ถูกต้องเกี่ยวกับปัญหาของจาวากับ generics การลบประเภทไม่ใช่สิ่งที่เลวร้ายจริง ๆ แล้วมันจำเป็นมากสำหรับประสิทธิภาพและมักใช้ในหลายภาษาเช่น C ++, Haskell, D.

ก่อนที่คุณจะรังเกียจโปรดจำคำจำกัดความของการลบประเภทที่ถูกต้องจากWiki

การลบประเภทคืออะไร

การลบประเภทหมายถึงกระบวนการโหลดซึ่งมีการเพิ่มความคิดเห็นประเภทชัดเจนออกจากโปรแกรมก่อนที่จะถูกดำเนินการในเวลาทำงาน

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

นี่คือสิ่งตอบแทนตามธรรมชาติ

ปัญหาของ Java นั้นแตกต่างกันและเกิดจากวิธีการแก้ไขใหม่

แถลงการณ์ที่เกิดขึ้นบ่อยครั้งเกี่ยวกับ Java ไม่ได้มีการแก้ไขซ้ำทั่วไปก็ผิดเช่นกัน

Java ทำใหม่ แต่ในทางที่ผิดเนื่องจากความเข้ากันได้ย้อนหลัง

การทำให้เป็นจริงคืออะไร?

จากWikiของเรา

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

การทำให้เป็นจริงหมายถึงการแปลงบางสิ่งที่เป็นนามธรรม (Parametric Type) เป็นสิ่งที่เป็นรูปธรรม (คอนกรีตประเภท) โดยความเชี่ยวชาญ

เราแสดงตัวอย่างนี้โดยตัวอย่างง่าย ๆ :

ArrayList พร้อมคำจำกัดความ:

ArrayList<T>
{
    T[] elems;
    ...//methods
}

เป็นนามธรรมโดยละเอียดตัวสร้างประเภทซึ่งได้รับ "reified" เมื่อมีความเชี่ยวชาญกับประเภทคอนกรีตพูดจำนวนเต็ม:

ArrayList<Integer>
{
    Integer[] elems;
}

ที่ArrayList<Integer>เป็นจริงชนิด

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

ArrayList
{
    Object[] elems;
}

ซึ่งจะแก้ไขที่นี่พร้อมกับ Object bound bound ( ArrayList<T extends Object>== ArrayList<T>)

แม้ว่ามันจะทำให้อาร์เรย์ทั่วไปใช้ไม่ได้และทำให้เกิดข้อผิดพลาดแปลก ๆ สำหรับประเภท raw:

List<String> l= List.<String>of("h","s");
List lRaw=l
l.add(new Object())
String s=l.get(2) //Cast Exception

มันทำให้เกิดความคลุมเครือมากมายเช่น

void function(ArrayList<Integer> list){}
void function(ArrayList<Float> list){}
void function(ArrayList<String> list){}

อ้างถึงฟังก์ชั่นเดียวกัน:

void function(ArrayList list)

ดังนั้นจึงไม่สามารถใช้วิธีการทั่วไปในการโหลดเกินใน Java


2

ฉันพบกับการลบประเภทใน Android ในการผลิตเราใช้ gradle พร้อมตัวเลือกขนาดเล็ก หลังจาก minification ฉันมีข้อยกเว้นร้ายแรง ฉันได้ทำฟังก์ชั่นที่เรียบง่ายเพื่อแสดงห่วงโซ่การสืบทอดของวัตถุของฉัน:

public static void printSuperclasses(Class clazz) {
    Type superClass = clazz.getGenericSuperclass();

    Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName()));
    Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString()));

    while (superClass != null && clazz != null) {
        clazz = clazz.getSuperclass();
        superClass = clazz.getGenericSuperclass();

        Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName()));
        Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString()));
    }
}

และมีสองผลลัพธ์ของฟังก์ชันนี้:

ไม่ใช่รหัสย่อ:

D/Reflection: this class: com.example.App.UsersList
D/Reflection: superClass: com.example.App.SortedListWrapper<com.example.App.Models.User>

D/Reflection: this class: com.example.App.SortedListWrapper
D/Reflection: superClass: android.support.v7.util.SortedList$Callback<T>

D/Reflection: this class: android.support.v7.util.SortedList$Callback
D/Reflection: superClass: class java.lang.Object

D/Reflection: this class: java.lang.Object
D/Reflection: superClass: null

รหัสย่อ:

D/Reflection: this class: com.example.App.UsersList
D/Reflection: superClass: class com.example.App.SortedListWrapper

D/Reflection: this class: com.example.App.SortedListWrapper
D/Reflection: superClass: class android.support.v7.g.e

D/Reflection: this class: android.support.v7.g.e
D/Reflection: superClass: class java.lang.Object

D/Reflection: this class: java.lang.Object
D/Reflection: superClass: null

ดังนั้นในรหัสที่ย่อขนาดคลาส parametrized จริงจะถูกแทนที่ด้วยประเภทคลาส raw โดยไม่มีข้อมูลประเภทใด ๆ ในฐานะที่เป็นทางออกสำหรับโครงการของฉันฉันได้ลบการเรียกการสะท้อนกลับทั้งหมดและแทนที่ด้วยประเภทพารามิเตอร์ที่ชัดเจนที่ส่งผ่านในอาร์กิวเมนต์ของฟังก์ชัน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.