Tại sao ví dụ không biên dịch, hay còn gọi là phương sai (co-, contra- và in-) hoạt động như thế nào?


147

Tiếp theo từ câu hỏi này , ai đó có thể giải thích những điều sau trong Scala:

class Slot[+T] (var some: T) { 
   //  DOES NOT COMPILE 
   //  "COVARIANT parameter in CONTRAVARIANT position"

}

Tôi hiểu sự khác biệt giữa +TTtrong khai báo kiểu (nó biên dịch nếu tôi sử dụng T). Nhưng sau đó, làm thế nào để một người thực sự viết một lớp đồng biến trong tham số loại của nó mà không dùng đến việc tạo ra thứ không bị biến dạng ? Làm thế nào tôi có thể đảm bảo rằng những điều sau đây chỉ có thể được tạo ra với một thể hiện T?

class Slot[+T] (var some: Object){    
  def get() = { some.asInstanceOf[T] }
}

EDIT - bây giờ có điều này xuống như sau:

abstract class _Slot[+T, V <: T] (var some: V) {
    def getT() = { some }
}

Điều này là tốt, nhưng bây giờ tôi có hai tham số loại, trong đó tôi chỉ muốn một tham số. Tôi sẽ hỏi lại câu hỏi như vậy:

Làm thế nào tôi có thể viết một lớp bất biến Slotcovariant trong loại của nó?

EDIT 2 : Duh! Tôi đã sử dụng varvà không val. Sau đây là những gì tôi muốn:

class Slot[+T] (val some: T) { 
}

6
Bởi vì varcó thể ổn định trong khi valkhông. Đó là cùng một lý do tại sao các bộ sưu tập bất biến của scala là đồng biến nhưng những bộ sưu tập có thể thay đổi thì không.
oxbow_lakes

Điều này có thể thú vị trong bối cảnh này: scala-lang.org/old/node/129
user573215

Câu trả lời:


302

Về mặt tổng quát, một tham số loại covariant là một tham số được phép thay đổi khi lớp được phân loại (thay vào đó, thay đổi theo phân nhóm, do đó tiền tố "co-"). Cụ thể hơn:

trait List[+A]

List[Int]là một kiểu con của List[AnyVal]Intlà một kiểu con của AnyVal. Điều này có nghĩa là bạn có thể cung cấp một ví dụ về List[Int]thời điểm giá trị của loại List[AnyVal]được mong đợi. Đây thực sự là một cách rất trực quan để thuốc generic hoạt động, nhưng hóa ra nó không có cơ sở (phá vỡ hệ thống loại) khi được sử dụng với sự có mặt của dữ liệu có thể thay đổi. Đây là lý do tại sao generic là bất biến trong Java. Ví dụ ngắn gọn về sự không chắc chắn bằng cách sử dụng các mảng Java (là sai số đồng biến):

Object[] arr = new Integer[1];
arr[0] = "Hello, there!";

Chúng tôi chỉ gán một giá trị của kiểu Stringcho một mảng kiểu Integer[]. Vì những lý do rõ ràng, đây là tin xấu. Hệ thống kiểu của Java thực sự cho phép điều này vào thời gian biên dịch. JVM sẽ "giúp đỡ" một cách hữu ích ArrayStoreExceptionkhi chạy. Hệ thống loại của Scala ngăn chặn vấn đề này bởi vì tham số loại trên Arraylớp là bất biến (khai báo [A]chứ không phải là [+A]).

Lưu ý rằng có một loại phương sai gọi là contravariance . Điều này rất quan trọng vì nó giải thích tại sao hiệp phương sai có thể gây ra một số vấn đề. Chống chỉ định theo nghĩa đen là đối lập với hiệp phương sai: các tham số thay đổi theo hướng phụ với phân nhóm. Nó ít phổ biến hơn một phần vì nó rất phản trực giác, mặc dù nó có một ứng dụng rất quan trọng: chức năng.

trait Function1[-P, +R] {
  def apply(p: P): R
}

Lưu ý chú thích phương sai " - " trên Ptham số loại. Tuyên bố này là toàn bộ một phương tiện mà Function1là contravariant trong Pvà hiệp biến trong R. Vì vậy, chúng ta có thể rút ra các tiên đề sau:

T1' <: T1
T2 <: T2'
---------------------------------------- S-Fun
Function1[T1, T2] <: Function1[T1', T2']

Lưu ý rằng T1'phải là một kiểu con (hoặc cùng loại) T1, trong khi nó ngược lại cho T2T2'. Trong tiếng Anh, điều này có thể được đọc như sau:

Một chức năng Một là một subtype của một chức năng B nếu kiểu tham số của A là một siêu kiểu của các loại tham số của B trong khi kiểu trả về của A là một subtype của kiểu trả về của B .

Lý do cho quy tắc này được để lại như một bài tập cho người đọc (gợi ý: suy nghĩ về các trường hợp khác nhau khi các hàm được phân loại, như ví dụ mảng của tôi ở trên).

Với kiến ​​thức mới được tìm thấy về đồng và chống chỉ định, bạn sẽ có thể thấy lý do tại sao ví dụ sau sẽ không biên dịch:

trait List[+A] {
  def cons(hd: A): List[A]
}

Vấn đề là Acovariant, trong khi conshàm dự kiến ​​tham số kiểu của nó là bất biến . Như vậy, Alà thay đổi hướng sai. Điều thú vị là đủ, chúng ta có thể giải quyết vấn đề này bằng cách làm Listcontravariant trong A, nhưng sau đó các kiểu trả về List[A]sẽ là không hợp lệ như các conschức năng hy vọng kiểu trả về của nó sẽ được hiệp biến .

Hai tùy chọn duy nhất của chúng tôi ở đây là a) tạo Abất biến, mất các thuộc tính gõ phụ trực quan, đẹp mắt của hiệp phương sai hoặc b) thêm một tham số kiểu cục bộ vào consphương thức xác định Alà giới hạn dưới:

def cons[B >: A](v: B): List[B]

Điều này là hợp lệ. Bạn có thể tưởng tượng rằng Ađang thay đổi đi xuống, nhưng Bcó thể thay đổi trở lên đối với các Atừ Alà nó thấp-bound. Với khai báo phương thức này, chúng ta có thể có Acovariant và mọi thứ đều ổn.

Lưu ý rằng thủ thuật này chỉ hoạt động nếu chúng ta trả về một thể Listhiện chuyên về loại ít cụ thể hơn B. Nếu bạn cố gắng làm cho Listcó thể thay đổi, mọi thứ sẽ bị hỏng do cuối cùng bạn cố gắng gán các giá trị của kiểu Bcho một biến kiểu A, không được trình biên dịch cho phép. Bất cứ khi nào bạn có khả năng biến đổi, bạn cần phải có một trình biến đổi nào đó, yêu cầu một tham số phương thức của một loại nhất định, (cùng với trình truy cập) ngụ ý bất biến. Hiệp phương sai hoạt động với dữ liệu bất biến vì hoạt động khả dĩ duy nhất là một bộ truy cập, có thể được cung cấp một kiểu trả về hiệp phương sai.


4
Điều này có thể được nói bằng tiếng Anh đơn giản như - bạn có thể lấy một cái gì đó đơn giản hơn làm tham số và bạn có thể trả lại một cái gì đó phức tạp hơn?
Phil

1
Trình biên dịch Java (1.7.0) không biên dịch "Object [] Array = new int [1];" nhưng thay vào đó đưa ra thông báo lỗi: "java: các loại không tương thích bắt buộc: java.lang.Object [] Found: int []". Tôi nghĩ bạn có nghĩa là "Đối tượng [] Array = new Integer [1];".
Emre Sevinç

2
Khi bạn đề cập, "Lý do cho quy tắc này được để lại như một bài tập cho người đọc (gợi ý: suy nghĩ về các trường hợp khác nhau khi các hàm được phân loại, như ví dụ mảng của tôi từ trên xuống)." Bạn thực sự có thể đưa ra một vài ví dụ?
perryzheng

2
@perryzheng mỗi này , mất trait Animal, trait Cow extends Animal, def iNeedACowHerder(herder: Cow => Unit, c: Cow) = herder(c)def iNeedAnAnimalHerder(herder: Animal => Unit, a: Animal) = herder(a). Sau đó, iNeedACowHerder({ a: Animal => println("I can herd any animal, including cows") }, new Cow {})không sao, vì người chăn gia súc của chúng ta có thể chăn bò, nhưng iNeedAnAnimalHerder({ c: Cow => println("I can herd only cows, not any animal") }, new Animal {})lại mắc lỗi biên dịch, vì người chăn bò của chúng ta không thể chăn dắt tất cả động vật.
Lasf

Điều này có liên quan và giúp tôi với phương sai: typelevel.org/blog/2016/02/04/variance-and-functor.html
Peter Schmitz

27

@Daniel đã giải thích nó rất tốt. Nhưng để giải thích ngắn gọn, nếu nó được cho phép:

  class Slot[+T](var some: T) {
    def get: T = some   
  }

  val slot: Slot[Dog] = new Slot[Dog](new Dog)   
  val slot2: Slot[Animal] = slot  //because of co-variance 
  slot2.some = new Animal   //legal as some is a var
  slot.get ??

slot.getsau đó sẽ đưa ra một lỗi trong thời gian chạy vì nó không thành công trong việc chuyển đổi Animalthành Dog(duh!).

Nói chung, tính đột biến không phù hợp với phương sai và phương sai. Đó là lý do tại sao tất cả các bộ sưu tập Java là bất biến.


7

Xem Scala bằng ví dụ , trang 57+ để thảo luận đầy đủ về điều này.

Nếu tôi hiểu chính xác nhận xét của bạn, bạn cần đọc lại đoạn văn bắt đầu ở cuối trang 56 (về cơ bản, điều tôi nghĩ bạn đang yêu cầu không an toàn về loại hình mà không kiểm tra thời gian, điều mà scala không làm, vậy là bạn đã hết may mắn rồi Dịch ví dụ của họ để sử dụng cấu trúc của bạn:

val x = new Slot[String]("test") // Make a slot
val y: Slot[Any] = x             // Ok, 'cause String is a subtype of Any
y.set(new Rational(1, 2))        // Works, but now x.get() will blow up 

Nếu bạn cảm thấy tôi không hiểu câu hỏi của bạn (một khả năng khác biệt), hãy thử thêm giải thích / bối cảnh vào mô tả vấn đề và tôi sẽ thử lại.

Đáp lại chỉnh sửa của bạn: Các vị trí bất biến là một tình huống hoàn toàn khác ... * cười * Tôi hy vọng ví dụ trên đã giúp ích.


Tôi đã đọc nó; thật không may, tôi (vẫn) không hiểu làm thế nào tôi có thể làm những gì tôi yêu cầu ở trên (nghĩa là thực sự viết một tham số lớp tham số trong T)
oxbow_lakes

Tôi đã xóa dấu vết của mình khi tôi nhận ra điều này là một chút khắc nghiệt. Tôi nên làm rõ trong (các) câu hỏi rằng tôi đã đọc các bit từ Scala bằng ví dụ; Tôi chỉ muốn nó được giải thích theo cách "ít trang trọng"
oxbow_lakes

@oxbow_lakes cười Tôi sợ Scala Ví dụ lời giải thích ít chính thức hơn. Tốt nhất, chúng ta có thể cố gắng sử dụng các ví dụ cụ thể để làm việc mặc dù ở đây ...
MarkusQ

Xin lỗi - tôi không muốn vị trí của mình có thể thay đổi. Tôi mới nhận ra rằng vấn đề là tôi đã khai báo var chứ không phải val
oxbow_lakes 19/03/2016

3

Bạn cần áp dụng một giới hạn thấp hơn trên tham số. Tôi đang gặp khó khăn khi nhớ cú pháp, nhưng tôi nghĩ nó sẽ trông giống như thế này:

class Slot[+T, V <: T](var some: V) {
  //blah
}

Scala-by-example là một chút khó hiểu, một vài ví dụ cụ thể sẽ có ích.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.