Bu makale, Kotlin’deki Jenerik ve Varyans kavramlarını kapsar ve Java ile karşılaştırır. Kotlin Generics, Java’nın kullanıcıların alt-yazarak ilişkilerinde nasıl davranacaklarını nasıl tanımlayabildiklerinden farklıdır. Java’nın aksine, Kotlin, bildirim alanında varyansı tanımlamaya izin verirken, Java yalnızca kullanım alanı varyansını bilir.
Kotlin Generics – Varyans Nedir?
Pek çok programlama dili, “A cat IS-an animal” gibi ilişkileri temsil eden hiyerarşilerin uygulanmasına izin veren alt yazma kavramını destekler. Java’da extends
mevcut bir sınıfın (kalıtımsal) davranışını değiştirmek / genişletmek için anahtar kelimeyi kullanabilir veya implements
bir arayüz için uygulamalar sağlamak için kullanabiliriz . Liskov’e göre en değiştirme prensibine , bir sınıfın her örneği A
kendi alt tipi örnekleri tarafından ikame edilmiş olabilir B
. Genellikle matematikte de ifade edilen varyans kelimesi, yöntem geri dönüş tipleri, tür bildirimleri, genel türler veya diziler gibi karmaşık yönlerde alt tiplemenin, ilgili sınıfların kalıtım derecesi ile nasıl ilişkili olduğunu açıklamak için kullanılır.. Dikkate almamız gereken üç terim var: Kovaryans, Kontravaryans ve Değişmezlik.
Uygulamadaki Varyans (Java)
Kovaryans
Java’da, geçersiz kılma yönteminin geri dönüş türünde kovaryant olması gerekir ; yani, geçersiz kılınan geri dönüş türleri ve geçersiz kılma yöntemi, ilgili sınıfların kalıtım ağacının yönü ile aynı hizada olmalıdır. Bir yöntem olup treat(): Animal
sınıfı AnimalDoctor
ile geçersiz kılınabilir treat(): Cat
sınıfında CatDoctor
uzanan, AnimalDoctor
. Başka bir kovaryans örneği, burada gösterilen tip dönüşümüdür:
public class Hayvanlar {}
public class Kedi extends Hayvan {}
(Hayvan) new Kedi() //Çalışıyor
(Kedi) new Hayvan() //Çalışmıyor
Kalıtım ağacında alt sınıflar oluşturabiliriz; Değişken bildirimlerine bakarsak, durum budur. Bir Cat
tür değişkeni örneği atamak sorun değil Animal
, bunun tersini yapmak başarısızlığa neden olur.
contravariance
Öte yandan, kontravaryans tam tersini tanımlar. Kontravaryans Java’da kullanılamaz. Bunu açıklığa kavuşturmak için, geçersiz kılma yöntemlerinde çelişkili yöntem argümanlarına izin veren başka bir programlama dili düşünebiliriz (Java’da bunun yerine aşırı yüklenmiş bir yöntem olabilir). Üst bir sınıf var diyelim ClassB
başka bir sınıfını genişleten ClassA
ve orijinal parametre türünü değiştirerek bir yöntemi geçersiz kılar T'
onun süper tipli T
.
ClassA::someMethod(t: T')
ClassB::someMethod(t: T)
Method parametresinin t
hiyerarşisinin, etrafındaki sınıfların hiyerarşisine aykırı olduğunu görebilirsiniz. Ağacın karşısında someMethod
, parametre türünde çelişkilidir.
değişmezlik
Son fakat en az değil, en kolay olanı: Değişmezlik. Java’daki yöntemleri geçersiz kılmayı düşündüğümüzde, daha önceki örnekte gördüğümüz gibi bu kavramı gözlemleyebiliriz. Geçersiz kılma yöntemi yalnızca geçersiz kılma yöntemiyle aynı parametreleri kabul etmelidir. Örneğin metot parametrelerinin üst ve alt tipte farklı olması durumunda değişmezlikten söz ediyoruz .
Java’da koleksiyon türlerinin varyansı
Dikkate almak istediğimiz bir diğer husus, diziler ve diğer genel koleksiyon türleri. Java’daki diziler, türlerinde kovaryanttır; bu, bir String
s dizisinin, bir değişkene atanabileceği anlamına gelir Object []
.
Object [] arr = new String [] {"hello", "world"};
Fakat daha önemlisi, diziler tuttukları türde kovaryanttır. Bu, bir Nesneye Tamsayı, Dizeleri veya ne tür bir Nesne ekleyebileceğiniz anlamına gelir [].
Object [] arr = new Object [2];
arr[0] = 1;
arr[1] = "2";
Kovariant dizileri oldukça kullanışlı görünebilir ancak çalışma zamanında hatalara neden olabilir. Bir tür değişken Object []
muhtemelen bir nesneye başvurabilir String []
. Değişkeni bir Nesneler dizisi bekleyen bir yönteme iletirsek ne olur? Bu yöntem, parametrenin türünde olması beklendiğinden yasal gibi görünen diziye bir Nesne eklemek isteyebilir Object []
. Sorun, arayan kişinin, ArrayStoreException
bu basitleştirilmiş örnekte gördüğümüz bir çalışma zamanına neden olabilecek dizisine koymak için hangi yöntemin muhtemel olacağı hakkında hiçbir fikrinin olmamasıdır :
Object [] arr = new String [] {"hello", "world"};
arr[1] = new Object(); //throws: java.lang.ArrayStoreException: java.lang.Object
Genel Koleksiyonlar
Java 1.5 itibarıyla jenerik (yani elemanları özel bir koleksiyon örneğin depolanabilir gerekiyordu derleyici bilgilendirmek için kullanılabilir List
, Map
, Queue
, Set
, vs.). Dizilerin aksine, genel koleksiyonlar varsayılan olarak parametreleştirilmiş türlerinde değişmez . Bu, a List<Animal>
ile değiştiremeyeceğiniz anlamına gelir List<Cat>
. Derleme bile olmaz. Sonuç olarak, genel koleksiyonları kovaryant dizilerden ( Etkili Java ) daha güvenli kılan beklenmeyen çalışma zamanı hatalarıyla karşılaşmak mümkün değildir . Bir dezavantajı olarak, ilk önce koleksiyonların alt tiplenmesi konusunda esnek değiliz. Neyse ki, kullanıcı generics kullanırken açıkça tür parametrelerinin varyansını belirleyebilir. Bu kullanım alanı varyansı diyoruz .
Kovariant koleksiyonları
Aşağıdaki kod örneği, bir kovaryant listesini nasıl ilan ettiğimizi Animal
ve bir listesini nasıl atadığımızı gösterir Cat
.
List<Cat> cats = new ArrayList<>();
List<? extends Animal> animals = cats;
Bunun gibi bir kovaryant liste, bir diziden hala farklıdır çünkü kovaryans, type parametresinde kodlanmıştır, bu da okuyucu için daha belirgin hale getirir. Sadece listeden okuyabiliriz, oysa derleyici öğe eklemeyi yasaklar. Liste bir olduğu söylenir Yapımcı ait Animal
s. Genel tür ? extends Animal
( ?
“Joker” karakteridir) sadece liste bir üst sınır ile her tür içerdiğini gösterir Animal
bir liste anlamına gelebilir, Cat
, Dog
ya da başka bir hayvan. Bu yaklaşım, kovaryant dizilerde karşılaşılan çalışma zamanı hatasını tercih edilen derleme hatasına dönüştürür:
public void processAnimals(List<? extends Animal> collection){
Animal a = collection.get(0);
Cat c = collection.get(0); //will not compile
collection.add(new Dog()) //will not compile
}
Şimdi, eğer arayanlar processAnimals
listesiyle Cat
çağırdıysanız, işlevin buna hiçbir şey eklemeyeceğinden emin olabilirsiniz. İşlev, yalnızca Animal
koleksiyondakileri alabilir , çünkü joker karakter sözdiziminin gösterdiği şey budur.
Kontravariant koleksiyonlar
Genel bir tür parametresi ? super Animal
(alt tür sınırı Animal
) ile bildirdiğimiz çelişkili koleksiyonlarla çalışmak da mümkündür . Böyle bir liste tipte olabilir List<Animal>
kendisi ya da herhangi bir süper tip bir listesi Animal
bile Object
. Çok değişkenli listelerde olduğu gibi, listenin gerçekte hangi türü temsil ettiğini bilemeyiz (yine joker karakter tarafından belirtilir). Aradaki fark, çelişkili bir listeden okuyamayacağımızdır, zira Animal
sadece s elde edip etmeyeceğimiz belirsizdir Object
. Orijinal arayanın Animal
herhangi bir alt türü eklemeyi mümkün kılan ya da onun süper tiplerini beklediğini bilmemize rağmen listeye yazmaya izin verilir Animal
. Listesi gibi davranmaktadır Tüketicinin ait Animal
s:
public void addAnimals(List<? super Animal> collection) {
collection.add(new Cat());
collection.add(new Dog());
}
Yukarıda gösterilen fonksiyon, çelişkili bir Animal
s listesini kabul eder . Bu işlevin bir arayanı olarak bir List<Animal>
ve ayrıca List<Object>
veya diğer olası alt tiplerin listelerini geçebilirsiniz Animal
. Koleksiyonunuza yalnızca hayvanlar veya alt türlerinin ekleneceğinden emin olabilirsiniz; bu, yöntem çağrısından sonra bile hayvanları listeden almak için güvenli olduğunuz anlamına gelir:
List<Animal> animals = new ArrayList<>();
addAnimals(animals);
//here we can still safely get Animals from the list, we don't care which subtype it actually has
Animal someAnimal = animals.get(0);
addAnimals(new ArrayList<Object>());
Joshua Bloch onun fantastik kitapta Pratik bir kural oluşturdu Etkili Java : “Yapımcı-uzanır tüketici süper (PECS)” kovaryant üretici ve arasındaki ilişkiyi ezberlemek yardımcı extends
anahtar kelime yanı sıra kontravaryant tüketiciler arasındaki super
anahtar kelime.
Kotlin Generics ile Varyans
Genel olarak varyansın ne anlama geldiğine ve Java’nın bu kavramları nasıl kullandığına bir göz attıktan sonra, şimdi Kotlin’in nasıl işlediğine bir göz atmak istiyoruz. Kotlin, jeneriklere gelince, dizilerle de kombinasyon halinde, birkaç yönden Java’dan farklıdır ve ilk bakışta deneyimli bir Java geliştiricisine garip gelebilir. İlk ve belki de en rahat fark şudur: Kotlin’deki diziler değişmezdir . Sonuç olarak, Java’nın aksine Array<String>
, türünde bir referans değişkenine atamak mümkün değildir Array<Object>
. Bu, derleme zamanı güvenliği sağladığı ve Java’da kovaryant dizileriyle karşılaşabileceğiniz çalışma zamanı hatalarını önlediği için harikadır. Ancak, daha sonra inceleyeceğimiz alt sıralı dizilerle veya kursla çalışmanın başka bir yolu olması gerekir.
Beyanname Site Varyansı
Daha önce de gösterildiği gibi, Java, jenerik ürününü çeşitlendirmek için “joker tip” olarak adlandırılan jantları kullanmaktadır; bu, [Java’nın tür sisteminin en zor kısmı] olduğu söylenmektedir (http://kotlinlang.org/docs/reference/generics .html # tip-projeksiyonları). Belirli genel türlerin kullanıcısı, belirli bir tür her kullanıldığında varyansı işlemesi gerektiğinden, bunu kullanım alanı varyansı olarak adlandırırız . Kotlin joker karakter kullanmıyor. Bunun yerine, Kotlin’de bildirim alanı varyansı kullanıyoruz . İlk problemi tekrar hatırlayalım: Hayal edin, ReadableList<E>
basit bir üretici yöntemiyle bir sınıfımız olduğunu düşünün get(): T
. Java, bir ReadableList<String>
tür değişkenin örneğinin atanmasını yasaklar ReadableList<Object>
çünkü genel türler varsayılan olarak değişmezdir. Bunu düzeltmek için kullanıcı değişken türünü olarak değiştirebilir.ReadableList<? extends Object>
ve her şey iyi çalışıyor. Kotlin bu konuya farklı yaklaşıyor. Türü T
yalnızca out
anahtar kelime ile oluşturulmuş olarak işaretleyebiliriz, böylece derleyici derhal anlayabilecektir: ReadableList
hiçbir zaman tükenmeyecek T
, bu onu kovariant yapan T
.
abstract class ReadableList<out T> {
abstract fun get(): T
}
fun workWithReadableList(strings: ReadableList<String>) {
val objects: ReadableList<Any> = strings // This is OK, since T is an out-parameter
// ...
}
Gördüğünüz T
gibi out
tür, bildirim alanı varyansı aracılığıyla tür olarak açıklanmıştır – varyans açıklaması olarak da adlandırılır. Derleyici T
kovaryant tip olarak kullanılmasını yasaklamaz . Standart kütüphanedeki kovaryant koleksiyonun harika bir örneği List<T>
:
val ints: List<Int> = listOf(1, 2, 3, 4)
fun takeNumbers(nums: List<Number>) {
val number: Number = nums[0]
nums.add(1) // add is not visible
}
takeNumbers(ints)
Bir üreticiyi List<Int>
kabul eden bir işleve kolayca geçebilirsiniz, List<Number>
çünkü List
yalnızca bir üreticidir T
ve takeNumbers
sınıfa asla bir şey eklemez. Öte yandan, parametre nums
türüyle tanımlanmışsa MutableList<Number>
, işlem add
iyi sonuç verirdi, ancak arayan kişi artık geçemedi List<Int>
ve yöntemin içine girilen listeye malzeme ekleyebileceği açıktı.
Tabii ki, onları kontravaryant yapmak, yani tüketiciler gibi jenerik türlerini işaretlemek için karşılık gelen bir açıklama da var: in
. Millet, sunulan yaklaşımı C # ile birkaç yıldır başarıyla kullanıyor.
Kotlin ezberlemek için kural: Üretici, Tüketici
Kullanım alanı Varyansı, Tür projeksiyonları
Ne yazık ki, bir tür parametresini T
ya out
da olarak bildirme fırsatına sahip olmak her zaman yeterli değildir in
. Mesela sadece dizileri düşünün. Bir dizi, nesneler eklemek ve almak için yöntemler sunar, bu yüzden kendi type parametresinde kovariant veya contravariant olamaz. Bir alternatif olarak, Kotlin da zaten tanımlanmış anahtar kelimeler kullanarak uygulayabilirsiniz kullanım yerinde varyansı verir in
ve out
:
Array<in String>
Java’nın tekabül Array<? super String>
ve Array<out String>
Java en tekabülArray<? extends Object>
fun copy(from: Array<out Any>, to: Array<Any>) {
// ...
}
Örnek, from
kendi türünde bir üretici olarak nasıl ilan edildiğini gösterir ve bu nedenle kopya yöntemi Diziye ekleme gibi kötü şeyler yapamaz. Bu kavram, dizi yöntemlerinde kısıtlandığı için tip projeksiyonu olarak adlandırılır : yalnızca tip parametresini döndüren yöntemler T
çağrılabilir.
Birleştirilmiş Türleri
Java’da olduğu gibi, genel türler yalnızca derleme zamanında kullanılabilir ve çalışma zamanında silinir . Tip güvenliğini sağlamak için jenerik kullandığımız için bu çoğu kullanım için yeterince iyidir. Bazen, çalışma zamanında gerçek tür bilgisini de alabilmek harikadır. Neyse ki, Kotlin , çalışma zamanında genel türler üzerinde çalışmayı mümkün kılan (örneğin bir tür kontrolü gerçekleştirme), birleştirilmiş türler adı verilen bir özellikle birlikte gelir . Bu istifleme makalesinde bu özelliği ayrıntılı olarak açıkladım ve bununla ilgili bir blog yazısı yayınladım .
Alt çizgi
Bu makalede, jenerik bağlamında varyansın oldukça karmaşık yönlerine baktık. Java’yı kovaryans, kontravaryans ve değişmezlik kavramlarını göstermek için kullandık ve Kotlin ile karşılaştırdık. Kotlin, bildirim alanı varyansı gibi farklı yaklaşımları kullanarak jenerikliği basitleştirmeye çalışır ve ayrıca daha belirgin anahtar kelimeler sunar ( in
,out
). Bence Kotlin, jenerik ilaçların kullanımını gerçekten geliştirir ve basitleştirir ve ayrıca kovaryant diziler sorununu da ortadan kaldırır. Beyanname site varyansı, Java’nın joker karakter sözdiziminde bilindiği gibi karmaşık bildirimler kullanmasını sağlayarak müşteri kodunu çok basitleştirir. Ayrıca, kullanım alanı değişkenliğine geri dönmemiz gerekse bile, sözdizimi daha net ve anlaşılması daha kolay görünür. Bu konunun en basit konu olmadığını biliyorum, ama umarım, bu makalede bazı hususlar biraz daha netleştirilmiştir.
Wow, marvelous weblog structure! How lengthy have you been blogging for?
you made running a blog look easy. The overall glance of your website is excellent, let alone
the content!