สวัสดีคนรักทั่วไปและประเภทความปลอดภัย
ฉันอยากเขียนบทความเกี่ยวกับ Generics & Variance มาเป็นเวลานานแล้ว แต่ก็ไม่สามารถเริ่มได้ มีบทความและวิดีโอดีๆ มากมายในหัวข้อนี้ ซึ่งฉันไม่สามารถคิดหาวิธีเพิ่มมูลค่าได้ แต่ที่นี่ฉันกำลังพยายาม หากคุณเป็นผู้เชี่ยวชาญ คุณสามารถตรวจสอบเนื้อหาด้านล่างนี้ได้ หรือหากคุณเป็นมือใหม่ คุณอาจได้รับสิ่งใหม่ๆ บ้าง เริ่มกันเลย.
ข้อมูลทั่วไป พวกเขาคืออะไร?
แนวคิดของ Generics ริเริ่มครั้งแรกในภาษาการเขียนโปรแกรมที่เรียกว่า "ML" (Meta Language) ในปี 1973 แนวทางนี้ทำให้โปรแกรมเมอร์สามารถเขียนฟังก์ชันทั่วไปที่แตกต่างกันเฉพาะกับประเภทที่ฟังก์ชันเหล่านั้นทำงานอยู่เท่านั้น ซึ่งช่วยลดความซ้ำซ้อนของโค้ด เช่น:
// Standard ML // A generic swap function fun swap (y, x) = (x, y)
ฟังก์ชัน swap
ในที่นี้อยู่ในประเภท 'a * 'b -> 'b * 'a
ไวยากรณ์ที่ดูแปลกตานี้ใช้เพื่ออธิบายประเภททั่วไปใน ML และภาษาอื่นๆ ที่ได้รับแรงบันดาลใจคล้ายกัน 'a
เป็นตัวแปรประเภท ซึ่งแสดงถึงประเภทที่เป็นไปได้ อาจเป็น Int, Float, String หรือประเภทเฉพาะใดๆ ที่กำหนดไว้ ณ เวลาที่กรณีการใช้งานหรือการสร้างอินสแตนซ์ ฟังก์ชั่นที่คล้ายกันใน Java & Swift จะเป็น:
// Java public <A, B> Pair<B, A> swap(Pair<A, B> pair) { return Pair.create(pair.second, pair.first); } // Swift func swap<A, B>(_ pair: (A, B)) -> (B, A) { return (pair.1, pair.0) }
ที่นี่ Pair<A,B>
ในตัวอย่าง Java คือ ประเภททั่วไป ซึ่งสามารถเก็บได้สองประเภท A
& B
เรียกว่า พารามิเตอร์ประเภท ก่อนที่จะมี Generics เราจะต้องเขียน IntPair
, StringPair
, int_swap
, string_swap
แยกกันสำหรับประเภทที่ไม่ซ้ำกันทุกประเภท
ประเภทพาราเมตริกที่คลุมเครือเหล่านี้เรียกว่า Generics ใน C#, Java, Rust, Swift, TypeScript และอื่นๆ อีกสองสามรายการ ในภาษา C++ เรียกว่า Parametric Polymorphism
ข้อมูลทั่วไป แนะนำแนวคิดของพารามิเตอร์ประเภท ซึ่งทำให้สามารถออกแบบโครงสร้าง (คลาส อินเทอร์เฟซ) และฟังก์ชัน (เมธอด) ที่เลื่อนข้อกำหนดของหนึ่งประเภทขึ้นไปจนกว่าจะถึงเวลาของการสร้างอินสแตนซ์ด้วยโค้ดไคลเอนต์
ข้อมูลทั่วไป — พวกเขาทำงานอย่างไรภายใต้ประทุน?
ต่อไปนี้เป็นข้อดีบางประการของการเขียนโค้ดทั่วไป:
- การตรวจสอบประเภทที่แข็งแกร่งยิ่งขึ้นในเวลาคอมไพล์
- ช่วยให้เราสามารถเขียนโค้ดที่ใช้ได้กับหลายประเภทโดยมีพฤติกรรมพื้นฐานเหมือนกัน เช่น:
List<T>
- รหัสที่ปรับให้เหมาะสม
ผู้อ่านที่ฉลาดจะตั้งคำถามถึงจุดที่ 3 ในรายการด้านบนทันที เพิ่มประสิทธิภาพหรือไม่? ในสิ่งที่รู้สึก? จุดนี้ต้องมีการแก้ไข ควรระบุเป็น Optimized ByteCode แต่ก่อนอื่นเรามาจัดการกับสองประเด็นแรกกันก่อน
พิจารณาตัวอย่างโค้ดทั้งสองด้านล่างนี้ (ใน Java)
// Java List<String> words = new ArrayList<>(); words.add("Hello "); words.add("world!"); String s = words.get(0) + words.get(1); System.out.println(s); // Hello World // Java List words = new ArrayList(); words.add("Hello "); words.add("world!"); String s = ((String)words.get(0)) + ((String)words.get(1)) System.out.println(s); // Hello World
ตัวอย่างเหล่านี้สร้างรหัสไบต์ที่เหมือนกันทุกประการ ในความเป็นจริง Java Compiler แปลงตัวอย่างที่ 1 โดยใช้ Generics เป็นตัวอย่างที่ 2 คอมไพเลอร์ทำให้แน่ใจว่า List<String>
เก็บเฉพาะรายการ String
ณ เวลาคอมไพล์ ดังนั้นเราจึงไม่ประสบปัญหาขณะรันไทม์ เนื่องจากคอมไพเลอร์ดูแลการแคสต์อัตโนมัติ จึงป้องกัน ClassCastException
ใดๆ ที่อาจถูกส่งไปที่รันไทม์ ดังนั้นความปลอดภัยประเภทที่ดีกว่า นอกจากนี้ยังมีเรื่องของ List<T>
อ่านเป็น List of T
สำหรับวิธีการเช่น size()
บน List
สิ่งที่เราต้องมีคือการนับจำนวนองค์ประกอบ ไม่ใช่ประเภทพื้นฐานขององค์ประกอบเหล่านั้น ดังนั้นนามธรรมที่ดีกว่า ปราศจากข้อมูลเฉพาะประเภท เคล็ดลับนี้ใช้โดย Java Compiler เรียกว่า Type Erasure
โมโนมอร์ฟิเซชัน
มีอีกวิธีหนึ่งที่คอมไพเลอร์จะทำงานกับโค้ดทั่วไปได้ สามารถคอมไพล์ได้เพื่อสร้างฟังก์ชันทั่วไปเวอร์ชันพิเศษแยกกัน สับสนเหรอ? มาดูตัวอย่างง่ายๆ ใน Rust กัน
// Rust fn print_hash<T: Hash>(t: &T) { println!("The hash is {}", t.hash()) }
print_hash(&true); // instantiates T = bool print_hash(&12_i64); // instantiates T = Int64
คอมไพเลอร์ Java จะมีฟังก์ชัน print_hash
เพียงชุดเดียว แต่จริงๆ แล้วคอมไพเลอร์ Rust จะสร้างสำเนาของฟังก์ชันนี้สองชุดสำหรับอาร์กิวเมนต์ที่เป็นรูปธรรมแต่ละประเภท ข้อดีคืออะไร ข้อดีคือคอมไพเลอร์สามารถปรับโค้ดให้เหมาะสมตามประเภทคอนกรีตในภาพ ซึ่งหมายความว่าการโทรใดๆ ไปยัง print_hash
จะถูกต่อสาย ณ จุดที่โทร การเรียกการจัดส่งแบบคงที่โดยตรง ไปยังการใช้งานที่เกี่ยวข้อง ซึ่งจะช่วยประหยัดค่าทางอ้อมในการเรียกใช้ฟังก์ชัน สิ่งนี้เรียกว่า การทำให้เป็นโมโนมอร์ฟิก — การแปลงโค้ดโพลีมอร์ฟิกเป็นโค้ดโมโนมอร์ฟิก
// Compiled Code
__print_hash_bool(&true);
__print_hash_i64(&12_i64);
หากประโยชน์ดูเหมือนน้อย ให้พิจารณาประเภท: Array<Bool>
& Array<Int>
คอมไพเลอร์สร้างสำเนาพิเศษ: Array_Bool
& Array_Int
สำหรับ Bool & Int ตามลำดับ คอมไพเลอร์สามารถจัดวางหน่วยความจำตามขนาดของ Bool & Int ไว้ในบรรทัด (เมื่อเป็นไปได้) และประหยัดพื้นที่ แต่ละเมธอดบน Array
จะสร้างการบันทึกโค้ดพิเศษทางอ้อมไปยังการเรียกเมธอดเช่นเดียวกัน ดังนั้นโค้ดทั่วไปจึงรวดเร็วและมีประสิทธิภาพหน่วยความจำเหมือนกับว่าคุณเขียนโค้ดพิเศษด้วยลายมือโดยตรง การเปรียบเทียบเป็นอย่างไรบ้าง: ArrayList<Boolean>
ใน Java ขึ้นอยู่กับการใช้งาน และใช้หน่วยความจำมากกว่าใน Rust อย่างน้อย 16 เท่า
ดูเหมือนจะน่าทึ่งใช่ไหม มีข้อเสียเช่นกัน: Code Bloat&เวลาการคอมไพล์ช้า คอมไพเลอร์สร้างอินสแตนซ์สำเนาอิสระของโค้ดทั่วไปสำหรับแต่ละชุดที่แตกต่างกันของพารามิเตอร์ทั่วไปที่ถูกเรียกใช้ นำไปสู่ขนาดไบนารีขนาดใหญ่และคอมไพล์ฟังก์ชันเดียวกันซึ่งอาจหลายครั้ง
ความปลอดภัยประเภททั่วไป
หากคุณมาถึงจุดนี้ คุณก็รู้แล้วว่าประเภทความปลอดภัยคืออะไร (เวลาคอมไพล์) การตรวจจับข้อผิดพลาดก่อนที่โปรแกรมจะรันเป็นสิ่งที่ดีเสมอไป ข้อมูลทั่วไปช่วยให้เราเขียนรหัสเซฟประเภทที่นำมาใช้ซ้ำได้ ลองพิจารณาตัวอย่างจากหนังสือ "Effective Java":
// Java // 1 private final Collection stampsUntyped = ... ; stampsUntyped.add(new Coin()) // 2 private final Collection<Stamp> stampsTyped = ... ; stampsTyped.add(new Coin()) ^ Type Mismatch
มีสองวิธีที่เราสามารถสร้างคอลเลกชันแสตมป์ได้
ใน 1 เรามีคอลเลกชันดิบซึ่งหมายความว่าสามารถเก็บอะไรก็ได้ มีความเป็นไปได้ที่เราอาจเพิ่มอินสแตนซ์ของคลาส Coin
ลงไป มันจะคอมไพล์ได้สำเร็จและจะล้มเหลวในขณะรันไทม์เมื่อเราดึงข้อมูล stamp
จากคอลเล็กชัน
ใน 2 เรามีคอลเลกชัน Generic โดยที่พารามิเตอร์ทั่วไปถูกกำหนดให้เป็น Stamp
นี่เป็นสิ่งที่ดี นี่เป็นประเภทที่ปลอดภัย ไม่มีทางที่เราจะเพิ่มอินสแตนซ์ของ Coin
ให้กับคอลเลกชันแสตมป์ได้ คอมไพเลอร์เข้ามาช่วยเหลือเราและไม่ยอมให้โค้ดนี้คอมไพล์
ลองนึกภาพใครบางคนใส่อินสแตนซ์ java.util.Date
ลงในคอลเลกชันที่ควรมีเพียง java.sql.Date
อินสแตนซ์ ประเภทที่มีพารามิเตอร์จะป้องกันสิ่งนี้ในเวลารวบรวม
หากคุณใช้ประเภท Raw คุณจะสูญเสียประโยชน์ด้านความปลอดภัยและความชัดเจนของ Generics ทั้งหมด
___________________________________________________________________
คำถาม:
อะไรคือความแตกต่างระหว่าง List
และ List<Object>
?
(ไม่ต้องกังวล เราจะตอบคำถามนี้ในไม่ช้า)
___________________________________________________________________
เดี๋ยวก่อน เราต้องการความปลอดภัยประเภทเสมอไปหรือเปล่า?
พิจารณาข้อมูลโค้ดนี้ (อีกครั้งจากหนังสือ Java ที่มีประสิทธิภาพ):
// Java static int numElementsInCommon(Set s1, Set s2) { int result = 0; for (Object o1 : s1) if (s2.contains(o1)) result++; return result; }
เมธอดนี้ numElementsinCommon
คำนวณจำนวนองค์ประกอบทั่วไประหว่างสองชุด ไม่จำเป็นต้องทำให้ ชุด อินพุตเหล่านี้เป็นแบบทั่วไป เนื่องจากเราเพียงเข้าถึงและเปรียบเทียบองค์ประกอบระหว่างกันเท่านั้น ดูเหมือนว่าเราไม่ต้องการความปลอดภัยเสมอไป
แต่เราสามารถทำให้ดีขึ้นด้วย Generics ได้หรือไม่ ถ้าใช่ ดีกว่าอย่างไร
คำตอบคือใช่ เราทำได้โดยใช้ Wildcard(?
) นี่ไม่ใช่เครื่องหมายคำถามไม่ใช่ ใน Java เรียกว่า Wildcard
อดทนไว้ อีกไม่นานเราจะได้เป็นไวด์การ์ด ก่อนอื่นเราต้องเข้าใจหลักการบางประการก่อน
การพิมพ์ย่อยและการทดแทนหลักการ (SP)
การพิมพ์ย่อย จะต้องดูเหมือนเป็นดินแดนที่คุ้นเคยหรือรู้จัก และจริงๆ แล้วเป็นเช่นนั้น มันเป็นคุณสมบัติหลักของภาษาเชิงวัตถุเช่น Java และเราใช้บ่อยเกินไป เมื่อเรา extend
หรือ implement
ประเภทใดประเภทหนึ่ง ประเภทที่เฉพาะเจาะจงมากขึ้น (เช่น ประเภทย่อย/ประเภทย่อย) สามารถใส่ในคอนเทนเนอร์ที่ใหญ่กว่าได้ (เช่น ประเภทหลัก/ประเภทพิเศษ) ตัวอย่างเช่น:
- จำนวนเต็มเป็นประเภทย่อยของตัวเลข
- Double เป็นประเภทย่อยของ Number
- String เป็นประเภทย่อยของ Object
- String[] เป็นประเภทย่อยของ Object[]
- ArrayList เป็นประเภทย่อยของ List
ต่อไปนี้เราจะแสดง A -> B
เป็น A is a subtype of B
& A !-> B
เป็น A is NOT a subtype of B
และ หลักการทดแทน พูดว่า:
หาก
S -> T
หมายความว่าคำใดๆ ประเภทS
สามารถใช้ได้อย่างปลอดภัยในบริบทที่ คาดว่าจะมีการใช้คำประเภทT
ตอนนี้คุณจำชื่อบทความนี้ได้แล้ว ฉันรู้ว่านี่เป็นเวลานาน แต่ถึงเวลาเริ่มต้นการเดินทางของเราไปยังส่วนที่น่าสนใจ —ความแปรปรวน ลองพิจารณาลำดับชั้นของชั้นเรียนด้านล่างสำหรับการสนทนาทั้งหมดที่รออยู่ข้างหน้า ตัวอย่างทั้งหมดจะอยู่ในภาษา Java
// Java public class Animal { String name; int troubleMakerRating; Animal(String name) { this.name = name; this.troubleMakerRating = new Random().nextInt(); } } public class Dog extends Animal { Dog(String name) { super(name); } } public class Cat extends Animal { Cat(String name) { super(name); } }
มีคลาส Animal
Dog
& Cat
ขยาย Animal
เนื่องจากสุนัข เป็น สัตว์ แมว เป็น สัตว์ Animal
แต่ละตัวมี name
และ troubleMakerRating
ดังนั้นตามหลักการทดแทน (SP) หากเรามีคอลเลกชั่นสัตว์ ก็อนุญาตให้เพิ่มตัวอย่างสุนัขหรือแมวเข้าไปได้:
List<Animal> animals = new ArrayList<Animal>(); animals.add(new Animal("Some Lion")); animals.add(new Dog("German Shepherd")); animals.add(new Cat("RagDoll"));
การพิมพ์ย่อยที่นี่มีสองรูปแบบ:
1. อนุญาตให้เพิ่มลงในรายการได้เนื่องจาก Dog/Cat is a subtype of Animal
.
2. อนุญาตให้ดำเนินการมอบหมายได้เนื่องจาก ArrayList is a subtype of List
ดังนั้น ถ้า Dog -> Animal
, ArrayList -> List
มันดูสมเหตุสมผลมากที่ ArrayList<Dog> -> List<Animal>
มาเขียนสิ่งนี้ลงในโค้ด:
List<Animal> animals = new ArrayList<Dog>();
ลองรวบรวมโค้ดด้านบน มันจะไม่. คอมไพเลอร์พ่น Incompatible Types
คอมไพเลอร์ไม่อนุญาตให้เราแทนที่ List<Animal>
ด้วย ArrayList<Dog>
เพราะมันไม่ปลอดภัย
List<Animal> animals = new ArrayList<Dog>(); // Compilation ERROR !! animals.add(new Cat("Birman"));
หากไม่มีข้อผิดพลาดในการคอมไพล์ที่บรรทัดแรก ภายใต้หลักการทดแทน จะอนุญาตให้เพิ่มอินสแตนซ์ Cat
ลงในรายชื่อสัตว์ซึ่งจริงๆ แล้วเป็นรายการ Dog
และ Cats and Dogs ไม่เข้ากัน ปัญหาจะเกิดขึ้นในเวลาดำเนินการเมื่อเราแยกวิเคราะห์รายชื่อสัตว์ที่คิดว่าพวกมันทั้งหมดเป็นสุนัขแต่เป็นแมวปรากฏขึ้น จำกฎ: ข้อผิดพลาดในการคอมไพล์คือดี ข้อผิดพลาดรันไทม์ไม่ดี นั่นเป็นสาเหตุที่หลักการทดแทนใช้ไม่ได้ที่นี่ เช่น ArrayList<Dog> !-> List<Animal>
แต่ก็มีบางกรณีที่เราต้องการให้ Substitution Principle ทำงาน เช่น เราต้องการให้ Compiler ผ่อนคลายสักหน่อยเพราะเรารู้ว่าเรากำลังทำอะไรอยู่
void printNames(Collection<Animal> animals) { animals.forEach(animal -> System.out.println(animal.name)); } List<Dog> myDogs = new ArrayList<>(); myDogs.add(new Dog("German Shepherd")); myDogs.add(new Dog("Bulldog")); myDogs.add(new Dog("Pug")); printNames(myDogs); // Compilation ERROR !! Incompatible Types
ฟังก์ชัน printNames
รับรายชื่อสัตว์และพิมพ์ชื่อสัตว์เหล่านั้น ดังนั้นเราจึงควรใช้ฟังก์ชันนี้ซ้ำเพื่อพิมพ์ชื่อของรายการสุนัข ท้ายที่สุดแล้ว ฟังก์ชัน printNames
เข้าถึงเฉพาะสัตว์จากรายการเท่านั้น และไม่ได้พยายามเขียนลงไป แต่งานของคอมไพเลอร์คือต้องปลอดภัย ดังนั้นจึงเกิดข้อผิดพลาดใน printNames(myDogs)
โอเวอร์คอมไพเลอร์อัจฉริยะ!!
และจะเป็นอย่างไรถ้าเราจำเป็นต้องเปรียบเทียบสุนัขสองตัวโดยใช้เครื่องเปรียบเทียบสัตว์ทั่วไป:
// 1 void printNameOfMostTroubleMakerDog(List<Dog> dogs, Comparator<Dog> comparator) { Dog maxTroubleMaker = dogs.get(0); for (int i = 1; i < dogs.size(); i++) { if (comparator.compare(maxTroubleMaker, dogs.get(i)) > 0) { maxTroubleMaker = dogs.get(i); } } System.out.println(maxTroubleMaker.name + " is the most trouble making Dog."); } // 2 Comparator<Animal> troubleMakerComparator = Comparator.comparingInt(animal -> animal.troubleMakerRating); // 3 - Compilation ERROR !! Incompatible Types printNameOfMostTroubleMakerDog(myDogs, troubleMakerComparator)
printNameOfMostTroubleMakerDog
รับรายชื่อสุนัขและพิมพ์ชื่อของสุนัขที่สร้างปัญหามากที่สุดโดยอิงจากตัวเปรียบเทียบอินพุต- เราสร้าง
troubleMakerComparator
ที่นำมาใช้ซ้ำได้ ซึ่งเปรียบเทียบคะแนนผู้สร้างปัญหาของสัตว์แต่ละตัว - ไม่มีการคอมไพล์ เนื่องจาก
printNameOfMostTroubleMakerDog
คาดว่าจะมีComparator<Dog>
แต่เรากำลังผ่านในComparator<Animal>
คอมไพเลอร์ควรอนุญาตสิ่งนี้ หาก Comparator<Animal>
สามารถเปรียบเทียบสัตว์สองตัวได้ ก็สามารถเปรียบเทียบสุนัขสองตัวได้อย่างแน่นอน
ตอนนี้คำถามก็เกิดขึ้น หากเรารู้ว่าการแทนที่บางอย่างข้างต้นนั้นปลอดภัย เราไม่ควรถ่ายทอดสิ่งนี้ให้คอมไพเลอร์ด้วยหรือ หากคำตอบของทุกคำถามคือ ไม่ ก็ไม่จำเป็นต้องมีบทความนี้ 😛
จำ ไวลด์การ์ด ( ?
) ใช่ เราจะใช้มันตอนนี้
ความแปรปรวน
ความแปรปรวนหมายถึงการพิมพ์ย่อยระหว่างประเภทที่ซับซ้อนมากขึ้นเกี่ยวข้องกับการพิมพ์ย่อยระหว่างส่วนประกอบอย่างไร ตัวอย่างเช่น รายการ Cats
ควรเกี่ยวข้องกับรายการ Animals
อย่างไร หรือฟังก์ชันที่ส่งคืน Cat
ควรเกี่ยวข้องกับฟังก์ชันที่ส่งคืน Animal
อย่างไร
มีความแปรปรวนที่เป็นไปได้สี่แบบ:
- ความแปรปรวนร่วม: รักษาลำดับของความสัมพันธ์ของการพิมพ์ย่อย เช่น
ถ้าA -> B
ดังนั้นBox<A> -> Box<B>
- ความขัดแย้ง: กลับลำดับของความสัมพันธ์ของการพิมพ์ย่อย เช่น
ถ้าA -> B
ดังนั้นBox<B> -> Box<A>
- ความแปรผัน: หากเป็นไปตามทั้งสองข้อข้างต้น เช่น
ถ้าA -> B
ดังนั้นBox<A> -> Box<B>
&Box<B> -> Box<A>
- ค่าคงที่: ไม่ใช้ ความแปรปรวนร่วม หรือ ความแปรปรวนร่วม
เพื่อแก้ปัญหาการทดแทนที่เราเผชิญเมื่อใช้ printNames
& printNameOfMostTroubleMakerDog
เราจะใช้ Covariance & Contravariance ตามลำดับ
ความแปรปรวนร่วม
หาก ArrayList<Dog>
กลายเป็นประเภทย่อยของ List<Animal>
ฟังก์ชัน printNames
จะสามารถนำมาใช้ซ้ำได้สำหรับรายการใดๆ ที่มีประเภทที่ขยาย Animal นี่เป็นวิธีที่พวกเราทำ:
void printNames(Collection<? extends Animal> animals) { ... } printNames(myDogs); // Compiles cleanly
ตอนนี้ printNames(myDogs);
ตรวจสอบประเภทและคอมไพล์ สังเกตการเปลี่ยนแปลงลายเซ็น printNames
ไม่ยอมรับ Collection<Animal>
อีกต่อไป แต่เป็น Collection<? extends Animal>
เมื่อเราใช้ ? extends
โดยพื้นฐานแล้วเรากำลังบอกคอมไพเลอร์ว่า คอลเลกชันของรายการที่มีขอบเขตบนเป็น Animal
ได้รับการยอมรับโดยฟังก์ชัน printNames
สิ่งนี้ทำสองสิ่ง:
- มันทำให้
List<Dog> -> Collection<Animal>
สิ่งนี้เรียกว่า ความแปรปรวนร่วม - และภายใน
printNames
เราสามารถ ไม่ เพิ่มรายการใด ๆ ลงในคอลเลกชัน แต่เราสามารถแยกวิเคราะห์หรือเข้าถึงรายการออกจากรายการได้ เราไม่สามารถเพิ่มได้เนื่องจากเป็นไปได้ว่ารายการนี้อาจเป็นรายชื่อสุนัข (รายชื่อที่เป็นเนื้อเดียวกัน) หรือรายชื่อสัตว์บางชนิด (รายชื่อที่ต่างกัน) เนื่องจากเราไม่แน่ใจว่ารายการสำคัญคืออะไร คอมไพลเลอร์จึงไม่อนุญาตให้เพิ่มคอลเลกชันใด ๆ
เรายังสามารถใช้ wildcard เมื่อประกาศตัวแปรได้ด้วย อดีต:
1| List<Dog> dogs = new ArrayList<Dog>(); 2| dogs.add(new Dog("PitBull")); 3| dogs.add(new Dog("Boxer")); 4| List<? extends Animal> animals = dogs; 5| animals.add(new Cat("Sphynx Cat")); // Compilation ERROR !!
หากไม่มี ? extends
บรรทัดที่ 4 จะทำให้เกิดข้อผิดพลาดในการรวบรวมเนื่องจาก List<Dog> !-> List<Animal>
แต่บรรทัดที่ 5 จะใช้งานได้เนื่องจาก Cat -> Animal
ด้วย ? extends
การคอมไพล์บรรทัดที่ 4 เนื่องจากตอนนี้ List<Dog> -> List<Animal>
แต่อันดับที่ 5 ไม่ได้คอมไพล์ เนื่องจากคุณไม่สามารถเพิ่ม Cat
ลงใน List<? extends Animal>
ได้ เนื่องจากอาจเป็นรายการประเภทย่อยอื่นๆ ของ Animal
วิธีทำความเข้าใจอีกวิธีหนึ่งคือ เมื่อเรา extend
มีประเภทขอบเขตบนเป็นอย่างน้อย ดังนั้นเราจึงรู้ว่าเรากำลังนำประเภทใดออกจากกล่อง ดังนั้น GET จึงได้รับอนุญาต เพราะนั่นเป็นตัวเลือกที่ปลอดภัยที่สุดสำหรับคอมไพเลอร์
กฎทั่วไป:
หากโครงสร้างมีองค์ประกอบที่มีประเภทรูปแบบ? extends E
เราสามารถ GET องค์ประกอบออกจาก โครงสร้าง แต่ เราไม่สามารถใส่ องค์ประกอบลงไปได้
ความขัดแย้ง
ในทำนองเดียวกัน เพื่อแก้ปัญหาตัวเปรียบเทียบ เราเปลี่ยนฟังก์ชัน printNameOfMostTroubleMakerDog
เพื่อรวมไวด์การ์ดและทำให้เป็น ? super Dog
เช่น:
void printNameOfMostTroubleMakerDog(List<Dog> dogs, Comparator<? super Dog> comparator) { ... } ... // Compiles Cleanly printNameOfMostTroubleMakerDog(myDogs, troubleMakerComparator);
ตอนนี้ printNameOfMostTroubleMakerDog(myDogs, troubleMakerComparator)
การตรวจสอบประเภท ? super Dog
หมายถึง ชนิดใดๆ ที่มีขอบเขตล่างเป็น Dog เช่น ชนิดใดๆ ที่เป็น supertype ของ Dog ดังนั้น Animal
& Object
จึงเป็นประเภทที่ยอมรับได้ที่นี่ นอกจากนี้ยังทำให้ Comparator<Animal>
เป็นประเภทย่อยของ Comparator<Dog>
สำหรับเมธอดนี้ นี่คือ ความขัดแย้ง จะต้องมีข้อพิสูจน์บางประการจากหลักการ GET-PUT ที่เราเห็นข้างต้น และที่จริงแล้วก็มีอยู่ ขณะนี้เราสามารถ วาง รายการภายในโครงสร้างดังกล่าวได้ แต่เราไม่สามารถ GET รายการออกได้ หากเป็นเช่นนั้น เราจะต้องมีตัวอย่างที่แตกต่างออกไป
public static void add_N_DogsTo(List<? super Dog> someList, int count) {
if (count == 0) return; for (int i = 0; i < count; i++) // Adding to the list is allowed someList.add(new Dog("Stray Dog " + i ));
// Reading from the list is NOT allowed. Compilation ERROR !! Animal d = someList.get(0); }
หากเรามีฟังก์ชันในการเพิ่มสุนัข 'N' ลงในบางรายการซึ่งมีขอบเขตล่างคือ Dog
ก็อนุญาตให้เพิ่มได้ แต่ไม่อนุญาตให้อ่านจากรายการ นี่เป็นเพราะว่าในระหว่าง PUT เรามีประเภทที่เป็นรูปธรรมที่เราสามารถเพิ่มได้ แต่ในระหว่างคำสั่ง GET เราไม่สามารถทราบประเภทของออบเจ็กต์เอาต์พุตได้ อาจเป็น Animal
หรือ Object
หมายเหตุ: Object d = someList.get(0);
ใช้งานได้เพราะใน Java ทุกอย่างขยาย Object จึงมีขอบเขตบนเสมอ
วิธีทำความเข้าใจอีกวิธีหนึ่งคือ เมื่อเรา super
มีประเภทขอบเขตล่างเป็นอย่างน้อย ดังนั้นเราจึงรู้ว่าประเภทที่เล็กที่สุดที่เราสามารถใส่ลงในช่องได้ ดังนั้น PUT จึงได้รับอนุญาต เพราะนั่นเป็นตัวเลือกที่ปลอดภัยที่สุดสำหรับคอมไพเลอร์
กฎทั่วไป:
หากโครงสร้างมีองค์ประกอบที่มีรูปแบบ? super E
เรา สามารถใส่ องค์ประกอบลงในโครงสร้างได้ แต่ เราสามารถทำได้ ไม่ได้รับ องค์ประกอบออกจากมัน
วุ้ย.. นั่นมันมากใช่มั้ย. มันเป็น แต่นั่นมันคน ก่อนที่เราจะแยกทางกัน เรามาตอบคำถามสองข้อที่เราทิ้งไว้ด้านบนกันดีกว่า
1) อะไรคือความแตกต่างระหว่าง List
และ List<Object>
?
- A
List
ได้ยกเลิกการใช้การตรวจสอบประเภททั่วไป โดยที่List<Object>
ได้บอกคอมไพเลอร์อย่างชัดเจนว่าสามารถเก็บรายการประเภทObject
ซึ่งโดยพื้นฐานแล้วคือทุกอย่างใน Java มันช่วยได้อย่างไร? อ่านประเด็นถัดไป: List list = new List<String>
เป็นไปได้ แต่List<Object> list = new List<String>
ไม่ เนื่องจาก ข้อมูลทั่วไปใน Java เป็นค่าคงที่ด้วยเหตุผลที่ได้กล่าวไว้แล้วในหลักการพิมพ์ย่อยและการทดแทน
<แข็งแกร่ง>2) เราจะทำให้ข้อมูลโค้ดต่อไปนี้ดีขึ้นได้อย่างไรโดยใช้ Generics โดยที่ไม่กังวลเรื่องความปลอดภัยของประเภท
// Java static int numElementsInCommon(Set s1, Set s2) { int result = 0; for (Object o1 : s1) if (s2.contains(o1)) result++; return result; }
หากต้องการทำซ้ำสิ่งที่ฉันกล่าวไว้ข้างต้น เราไม่จำเป็นต้องทำให้ชุดเป็นแบบทั่วไปที่นี่ (เช่น Set<T>
) เพราะเป็นเพียงการเปรียบเทียบองค์ประกอบของทั้งสองอย่างเท่านั้น แต่ถ้าเราเพิ่มไวด์การ์ดที่นี่:
// Java static int numElementsInCommon(Set<?> s1, Set<?> s2) { int result = 0; for (Object o1 : s1) if (s2.contains(o1)) result++; return result; }
การเพิ่ม wildcard ซื้ออะไรให้เราบ้าง?. Set
และ Set<?>
แตกต่างกันอย่างไร
เราได้ทำให้ฟังก์ชันนี้ปลอดภัยยิ่งขึ้นในการใช้งานมาก เมื่อใช้ Set
เราสามารถใส่องค์ประกอบใดก็ได้ภายในฟังก์ชันและแก้ไขเนื้อหาได้ แต่เนื่องจาก Generics เป็นค่าคงที่ใน Java Compiler จึงห้ามไม่ให้เราเพิ่มสิ่งใดใน Set<?>
ไม่ได้หมายความว่าเราไม่สามารถเรียก set.removeAll()
หรือ set.pop()
ภายในฟังก์ชันและไม่สามารถแก้ไขเนื้อหาได้ นอกจากนี้เรายังสามารถเพิ่ม null
ลงในคอลเลกชัน Set<?>
ได้ด้วย แต่ความปลอดภัยใดๆ ก็มีประโยชน์เสมอ
เสร็จแล้วก็ปัดฝุ่น หวังว่าการอ่านมาเป็นเวลานานนี้จะช่วยให้คุณเข้าใจว่าแนวคิดเหล่านี้เชื่อมโยงกันอย่างไร และเหตุใดการเรียนรู้ทั้งสองอย่างร่วมกันจึงเป็นเรื่องสำคัญ
ขอบคุณที่อ่านจนจบ กรุณาแบ่งปัน 🗣 ในแวดวงของคุณและปรบมือ 👏🏻 หากคุณชอบมัน
หากคุณมีข้อสงสัยหรือมีอะไรเพิ่มเติมหรือแก้ไขในบทความนี้ อย่าลังเลที่จะแสดงความคิดเห็น (ไม่มีเจตนาเล่นสำนวน) แล้วฉันจะกลับมา (อีกครั้ง ไม่มีเจตนาเล่นสำนวน 😛) กลับมาหาคุณโดยเร็วที่สุด .
ติดตาม "Androidiots" เพื่อดูเนื้อหาที่น่าทึ่งอีกมากมาย