kotlin基础学习笔记第九章——泛型

        实化类型参数允许你在运行时的内联函数中引用作为类型实参的具体类型(对普通的类和函数来说,这样行不通,因为类型实参在运行时会被擦除)

         声明点变型可以说明一个带类型参数的泛型类型,是否是另一个泛型类型子类型或者超类型,它们的基础类型相同但类型参数不同。例如,它能调节是否可以把List<Int>类型的参数传给期望List<Any>的函数。使用点变型在具体使用一个泛型类型时做同样的事,达到和java通配符一样的效果。

     一、泛型类型参数

       泛型允许你定义带类型形参的类型。当这种类型的实例被创建出来的时候,类型形参被替换成称为类型实参的具体类型。例如,如果有一个List类型的变量,弄清楚这个列表中可以存储哪种事物是很有意义的。类型形参可以准确清晰地进行描述,就像这样“这个变量保存了字符串列表”,而不是“这个变量保存了一个列表”。kotlin说明“字符串列表”的语法和java看起来一样:List<String>。还可以给一个类声明多个类型形参。例如,Map类型就有键类型和值类型两个类型形参:class Map<K, V>。我们可以用具体的类型实参来实例化它:Map<String, Person>。目前,所有概念都和java没什么不一样。

        和一般类型一样,Kotlin编译器也常常能推导出类型实参

val authors = listOf("Dmitry", "Svetlana")

         因为传给listOf函数的两个值都是字符串,编译器能推导出你正在创建一个List<String>。另一方面,如果你想创建一个空的列表,这样就没有任何可以推导出类型实参的线索,你就得显式地指定它(类型实参)。就创建列表来说,既可以选择在变量声明中说明泛型的类型,也可以选择在创建列表的函数中说明类型实参。参看下面的例子:

        1、泛型函数和属性

        如果要编写一个使用列表的函数希望它可以在任何列表(通用的列表)上使用,而不是某个具体类型的元素的列表,需要编写一个泛型函数。泛型函数有它自己的类型参数。这些类型形参在每次函数调用时都必须替换成具体的类型实参

        大部分使用集合的库函数都是泛型的。来看看slice函数:这个函数返回一个只包含在指定下标区间内的元素。

         接收者和返回类型用到了函数的类型形参T,它们的类型都是List<T>。当你在一个具体的列表上调用这个函数时,可以显式地指定类型的实参。但大部分情况下你不必这样做,因为编译器可以推导出类型。

        这两次调用的结果都是List<Char>。编译器把函数返回类型List<Char>中的T替换成了推导出来的类型Char。

        再来看看filter函数的声明,它接收了一个函数类型(T)-> Boolean的参数。

         这个例子中自动生成的lambda参数it的类型是String。编译器必须把它推导出来:毕竟,在函数声明中lambda参数是泛型类型T。编译器推断T就是String,因为它知道函数应该是list<T>上调用,而它的接收者readers的真实类型是List<String>。

        可以给类或接口的方法、顶层函数,以及扩展函数声明类型参数。

        不能声明泛型非扩展属性。

        2、声明泛型类

         和java一样,kotlin通过在类名称后加上一对尖括号,并把类型参数放在尖括号内来声明泛型类及泛型接口。一旦声明之后,就可以在类的主体内像其他类型一样使用类型参数。我们来看看标准java接口List如何使用Kotlin来声明。

        如果你的类继承泛型类(或者实现泛型接口),你就得为基础类型的泛型形参提供一个类型实参。它可以是具体类型或者另一个类型形参。 

         StringList类被声明成只能包含String元素,所以它使用String作为基础类型的类型参数。子类中的任何函数都要用这个正确的类型替换掉T。所以在StringList中你会得到函数签名get(Int): String,而不是fun get(Int): T。

        而类ArrayList定义了它自己的类型参数T并把它指定为父类的类型实参。注意ArrayList<T>中的T和List<T>中的T不一样,它是全新的类型形参,不必保留一样的名称

        一个类甚至可以把它自己作为类型实参引用。实现Comparable接口的类就是这种模式的经典例子。任何可以比较的元素都必须定义它如何与同样类型的对象比较:            String类实现了Comparable泛型接口,提供类型String给类型实参T。

        迄今为止,泛型和java中的看起来差不多。    

        3、类型参数约束

        类型参数约束可以限制作为(泛型)类和(泛型)函数的类型实参的类型。        

         如果你把一个类型指定为泛型类型形参的上界约束,在泛型类型具体的初始化中,其对应的类型实参就必须是这个具体类型或者它的子类型

        你是这样定义约束的,把冒号放在类型参数名称之后,作为类型形参上界的类型紧随其后。在java中用的是关键字extends来表达一样的概念:

        这次函数调用是允许的,因为具体类型实参(下面这个例子中是Int) 继承了Number。

        一旦指定了类型形参T的上界,你就可以把类型T的值当作它的上界(类型)的值使用。例如,可以调用定义在上界类中的方法:         现在让我们编写一个找出两个条目中最大值的泛型函数。因为只有在可以相互比较的条目之中才能找出最大值,需要在函数签名中说明这一点。做法如下:

        当你试图对不能比较的条目调用max方法时,代码不能编译:

        T的上界是泛型类型Comparable<T> 。前面已经看到了,String类继承了Comparable<T> ,这样使得String变成了max函数的有效类型实参。

        记住,first > second的简写形式会根据kotlin的运算符约定被编译成first.compareTo(second) > 0。这种比较之所以可行,是因为first的类型T继承自Comparable<T>,这样你就可以 比较first和另外一个类型T的元素。

        极少数情况下,需要在一个类型参数上指定多个约束,这时你需要使用稍微不同的语法。例如下面这个代码清单用泛型的方式保证给定的CharSequence以句号结尾。标准StringBuilder类和CharBuffer类都适用。

        这种情况下,可以说明作为类型实参的类型必须实现CharSequence和Appendable两个接口。这意味着该类型的值可以使用访问数据(endsWith)和修改数据(append)两种操作。

        4、让类型形参非空         

         如果你声明的是泛型类或者泛型函数,任何类型实参,包括那些可空的类型实参,都可以替换它的类型形参。事实上,没有指定上界的类型形参将会使用Any?这个默认的上界。来看看这个例子:

        process函数中,参数value是可空的,尽管T并没有使用问号标记。下面这种情况是因为Processor类具体初始化时T能使用可空类型:

         如果你想保证替换类型形参的始终为非空类型,可以通过指定一个约束来实现。如果你除了可空性之外没有任何限制,可以使用Any代替默认的Any?作为上界:

         约束<T: Any>确保了类型T永远都是非空类型。编译器不会接收代码Processor<String?>,因为类型实参String?不是Any的子类型(它是Any?的子类型,一种更普通的类型):

        注意,可以通过指定任意非空类型作为上界,来让类型参数非空,不光是类型Any。 

二、运行时的泛型:擦除和实化类型参数

        JVM上的泛型一般都是通过类型擦除实现的,就是说泛型类实例的类型实参在运行时是不保留的。在本节中,我们将讨论类型擦除对kotlin的实际影响,以及如何通过将函数声明为inline来解决其局限性。可以声明一个inline函数,使其类型实参不被擦除(或者,按照kotlin术语,称作实话)

1、运行时的泛型:类型检查和转换

        和java一样,kotlin的泛型在运行时也被擦除了。这意味着泛型类实例不会携带用于创建它的类型实参的信息。 例如,如果你创建了一个List<String>并将一堆字符串放到其中,在运行时你只能看到它是一个List,不能识别出列表本打算包含的是哪种类型的元素(当然,你也可以获取一个元素然后检查它的类型,但即便检查通过了也不会有任何保证,因为其他的元素可能拥有不同的类型)。   

         即使编译器看到的是两种完全不同类型的列表,在执行时他们看起来是完全一样的。尽管如此,你通常可以确信List<String>只包含字符串,而List<Int>只包含整数。因为编译器知道类型实参,并确保每个列表中只存储正确类型的元素(可以通过类型转换或使用java原生态类型访问列表,来欺骗编译器,但你需要特意这样做)。

        接下来我们来谈谈伴随着擦除类型信息的约束因为类型实参没有被存储下来,你不能检查它们。例如,你不能判断一个列表是一个包含字符串的列表还是包含其他对象的列表。一般而言,在is检查中不可能使用类型实参中的类型。下面这样的代码不会编译:

         尽管在运行时可以完全断定这个值是一个List,但你依然无法断定它是一个含有字符串的列表,还是含有人,或者含有其他什么:这些信息被擦除了注意擦除泛型类型信息是有好处的:应用程序使用的内存总量较小,因为要保存在内存中的类型信息更少。 

        如前所述,kotlin不允许使用没有指定类型实参的泛型类型。那么你可能想知道如何检查一个值是否是列表,而不是set或者其他对象。可以使用特殊的星号投影语法来做这种检查:        

if (value is List<*>) {...}

        实际上,泛型类型拥有的每个类型形参都需要一个*。你可以认为它就是拥有未知类型实参的泛型类型(或者类比于java的List<?>)。

        注意,在as和as?转换中仍然可以使用一般的泛型类型。但是如果该类有正确的基础类型但类型实参是错误的,转换也不会失败,因为在运行时转换发生的时候实参是未知的。因此,这样的转换会导致编译器发出”unchecked cast“(未受检转换)的警告。这仅仅是一个警告,你仍然可以继续使用这个值,就当它拥有必要的类型。

        编译一切正常:编译器只是发出了一个警告,这意味着代码是合法的。如果在一个整形的列表或者set上调用printSum函数,一切都会如预期发生:第一种情况会打印元素之和,而第二种情况则会抛出IllegalArgumentException。但如果你传递了一个错误类型的值,运行时会得到一个ClassCastException:

        我们来讨论下在字符串列表上调用printSum函数时抛出的异常。你得到的并不是 IllegalArgumentException,因为你没有办法判断实参是不是一个List<Int>。因此类型转换会成功,无论如何函数sum试着从列表中读取Number值然后把它们加在一起。把String当Number用的尝试会导致运行时的ClassCastException。

        注意,kotlin编译器是足够智能的,在编译期它已经知道相应的类型信息时,is检查是允许的。

        在上面代码中,c是否拥有类型List<Int>的检查是可行的,因为在编译器就确定了集合(不管它是列表还是其他类型的集合)包含的是整形数字

        通常,kotlin编译器会负责让你知道哪些检查是危险的(禁止is检查,以及发出的as转换的警告),而哪些又是可行的。你要做的就是了解这些警告的含义并且了解哪些操作时安全的。 

 2、声明带实化类型参数的函数

        前面已经讨论过,kotlin泛型在运行时会被擦除,这意味着如果你有一个泛型类的实例,你无法弄清楚在这个实例创建时用的究竟是哪些类型实参。泛型函数的类型实参也是这样。在调用泛型函数的时候,在函数中你不能决定调用它用的类型实参。

        通常情况下都是这样,只有一种例外可以避免这种限制:内联函数。内联函数的类型形参能够被实化,意味着你可以在运行时引用实例的类型实参

         如果使用inline关键字标记一个函数,编译器会把每一个函数调用都换成函数实际的代码实现。使用内联函数还可以提升性能,如果该函数使用了lambda实参:lambda的代码也会内联,所以不会创建任何匿名类

        如果你把上面的例子中的isA函数声明成inline并且用reified标记类型参数,你就能够用该函数检查value是不是T的实例。

        接下来看看使用实化类型参数的一些稍微有意义的例子。一个实化类型参数能发挥作用的最简单的例子就是标准库函数filterIsInstance。这个函数接收一个集合,选择其中哪些指定类的实例,然后返回这些被选中的实例。下面展示了这个函数的用法。

         通过指定<String>作为函数的类型实参,你表明感兴趣的只是字符串。因此函数的返回类型是List<String>。这种情况下,类型实参在运行时是已知的,函数filterIsInstance使用它来检查列表中的值是不是指定为该类型实参的类的实例。

        下面是kotlin标准库函数filterIsInstance声明的简化版本。

        为什么实化只对内联函数有效?

        一个内联函数可以有多个实化类型参数,也可以同时拥有非实化类型参数和实化类型参数。注意, filterIsInstance函数虽然被标记成inline,而它并不期望lambda作为实参。

 3、使用实化类型参数代替引用

        另一种实化类型参数的常见使用场景是为接收java.lang.Class类型参数的API构建适配器。一种这种API的例子是JDK中的ServiceLoader,它接收一个代表接口或抽象类的java.lang.Class,并返回实现了该接口(或继承了该抽象类)的类的实例。

        通过下面的调用来使用标准的ServiceLoader Java API加载一个服务:

val serviceImpl = ServiceLoader.load(Service::class.java)

        ::Class.java的语法展现了如何获取java.lang.Class对应的Kotlin类。这和java中的Service.class是完全相同的。

        现在让我们使用带实化类型参数的函数重写这个例子:

val serviceImpl = loadService<Service>()

        代码是不是短了不少?要加载的服务类现在被指定成了loadService函数的类型实参。把一个类指定成类型实参要容易理解得多,因为它的代码比使用::class.java语法更短。

        下面看看这个loadService函数时如何定义的:         这种用在普通类上的::class.java语法也可以同样用在实化类型参数上。使用这种语法会产生对应到指定为类型参数的类的java.lang.Class,你可以正常地使用它。

4、实化类型参数的限制

          尽管实化类型参数是方便的工具,但它们也有一些限制。有一些是实化与生俱来的,而另外一些则是现有的实现决定的,而且可能在未来的kotlin版本中放松这些限制。

        具体来说,可以按下面的方式使用实化类型参数:      

        不能做下面这些事情:

         最后一条限制会带来有趣的后果:因为实化类型参数只能用在内联函数上,使用实化类型参数意味着函数和所有传给它的lambda都会被内联。如果内联函数使用lambda的方式导致lambda不能被内联,或者你不想lambda因为性能的关系被内联,你可以使用noinline修饰符把它们标记成非内联的。

3、变型:泛型和子类型化

        变型的概念描述了拥有相同基础类型不同类型实参的(泛型)类型之间是如何关联的:例如,List<String>和List<Any>之间如何关联。

1、为什么存在变型:给函数传递实参

         假设你有一个接收List<Any>作为实参的函数。把List<String>类型的变量传给这个函数是否安全?毫无疑问,把一个字符串传给一个期望Any的函数时安全的,因为String类继承了Any。但是当String和Any变成List接口的类型实参之后,情况就没有这么简单了。

        例如,考虑一个打印列表内容的函数。

        看起来这里的字符串列表可以正常工作。函数把每个元素都当成Any对待,而且因为每个字符串都是Any,这是完全安全的。

        现在来看另外一个函数,它会修改列表(因此他接收一个MutableList作为参数):

        你声明了一个类型为MutableList<String>的变量strings,然后尝试把它传给这个函数。假设编译器接收了,你就能在字符串列表中添加一个整形,这会导致当你在运行时尝试访问列表中的字符串的时候出现异常。正因如此,这次调用不会编译通过。这个例子展示了当期望的是MutableList<Any>的时候把一个MutableList<String>当做实参传递是不安全的,kotlin编译器正确地禁止了它。

        现在可以回答你刚才的那个问题了,把一个字符串列表传给期望Any对象列表的函数是否安全。如果函数添加或者替换了列表中的元素就是不安全的,因为这样会产生类型不一致的可能性。否则它就是安全的。在kotlin中,这可以通过根据列表是否可变选择合适的接口来轻易地控制。如果函数接收的是只读列表,可以传递具有更具体的元素类型的列表。如果列表是可变的,你就不能这样做。

2、类、类型和子类型

        上节讨论过,变量的类型规定了该变量的可能值。有时候我们会把类型当成同样的概念使用,但它们不一样,现在是时候看看它们的区别了。

        最简单的例子就是非泛型类,类的名称可以直接当做类型使用。例如,如果你这样写 var x: String,就是声明了一个可以保存String类的实例的变量。但是注意,同样的类名称也可以用来声明可变类型: var x: String?。这意味着每个kotlin类都可以用于构造至少两种类型

        泛型类的情况就变得更复杂了。要得到一个合法的类型,需要用一个作为类型实参的具体类型替换(泛型)类的类型形参List不是一个类型(它是一个类),但是下面列举出来的所有替代品都是合法的类型:List<Int>、List<String>、List<List<String>>等每一个泛型类都可能生成潜在的无线数量的类型

        为了讨论类型之间的关系,需要熟悉子类型这个术语。任何时候如果需要的是类型A的值,你都能够使用类型B的值(当做A的值),类型B称为类型A的子类型。举例来说,Int是Number的子类型,但Int不是String的子类型。这个定义还表明了任何类型都可以被认为是它自己的子类型。

        术语超类型是子类型的反义词。如果A是B的子类型,那么B就是A的超类型。 

        为什么一个类型是否是另一个的子类型这么重要?编译器在每一次给变量赋值或者给函数传递实参的时候都要做这项检查。参考下面这个例子。

        只有值的类型是变量类型的子类型时,才允许变量存储该值。例如,变量n的初始化器i的类型Int是变量的类型Number的子类型,所以n的声明是合法的。只有当表达式的类型是函数参数的类型的子类型时,才允许把该表达式传给函数。这个例子中i的类型Int不是函数参数的类型String的子类型,所以函数f的调用会编译失败。 

        可空类型提供了一个例子,说明子类型和子类不是同一个事物,如下图所示。

        一个非空类型是它的可空版本的子类型,但它们都对应着同一个类。你始终能在可空类型的变量中存储非空类型的值,但反过来却不行(null不是非空类型的变量可以接收的值) 

         当我们开始涉及泛型类型时,子类型和子类之间的差异显得格外重要。前面一节的那个问题,把List<String>类型的变量传给期望List<Any>的函数是否安全,现在可以使用子类型化术语来重新组织:List<String>是List<Any>的子类型吗?你已经了解了为什么把MutableList<String>当成MutableList<Any>的子类型对待是不安全的。显然,反过来也是不成立的:MutableList<Any>肯定不是MutableList<String>的子类型。

        一个泛型类——例如,MutableList——如果对于任意两种类型A和B,MutableList<A>既不是MutableList<B>的子类型也不是它的超类型,它就被称为在该类型参数上是不变型的。java中所有的类都是不变型(尽管那些类具体的使用可以标记成可变型的,稍后你就会看到)

        在前一节中,你见过一个这样一个类,List,对它来说,子类型化规则不一样。kotlin中的List接口表示的是只读集合。如果A是B的子类型,那么List<A>就是List<B>的子类型。这样的类或者接口被称为协变的。下一小节会详细讨论协变的概念并解释什么时候才可以把类型或者接口声明成协变的。

3、协变:保留子类型化关系

        一个协变类是一个泛型类(我们以Producer<T>为例),对这种类来说,下面的描述是成立的:如果A是B的子类型,那么Producer<A>就是Producer<B>的子类型。我们说子类型化被保留了。例如,Producer<Cat>是Producer<Animal>的子类型,因为Cat是Animal的子类型。

        在kotlin,要声明类在某个类型参数上是可以协变的,在该类型参数的名称前加上out关键字即可

         将一个类的类型参数标记为协变的,在该类型实参没有精确匹配到函数中定义的类型形参时,可以让该类的值作为这些函数的实参传递,也可以作为这些函数的返回值。例如,想象一下有这样一个函数,它负责喂养用类Herd代表的一群动物(畜群),Herd类的类型参数确定了畜群中动物的类型。

        很遗憾,这群猫要挨饿了:如果尝试把猫群传给feedAll函数,在编译期你就会得到类型不匹配的错误。因为Herd类中的类型参数T没有用任何变型修饰符,猫群不是畜群的子类。可以使用显式的类型转换来绕过这个问题,但是这种方法啰嗦、易出错,而且几乎从来都不是解决类型不匹配的正确方式。

        因为Herd类有一个类似List的API,并且不允许它的调用者添加和改变畜群中的动物,可以把它变成协变的并相应地修改调用代码。

         不能把任何类都变成协变的:这样不安全。让类在某个类型参数变为协变,限制了该类中对该类型参数使用的可能性。要保证类型安全,它只能用在所谓的out位置,意味着这个类只能生产类型T的值而不能消费他们

        在类成员的声明中类型参数的使用可以分为in位置和out位置。考虑这样一个类,它声明了一个类型参数T并包含了一个使用T的函数。如果函数是把T当成返回类型,我们说他在out位置。这种情况下,该函数生产类型为T的值。如果T用作函数参数的类型,它就在in位置。这样的函数消费类型为T的值,如下图所示。 

        类的类型参数前的out关键字要求所有使用T的方法只能把T放在out位置而不能放在in位置。这个关键字约束了使用T的可能性,这保证了对应子类型关系的安全性

         以Herd类为例,它只在一个地方使用了类型参数T:get方法的返回值。

        这是一个out位置,可以安全地把类声明成协变的。如果Herd<Animal>类的get方法返回的是Cat,任何调用该方法的代码都可以正常工作,因为Cat是Animal的子类。

        重申一下,类型参数T上的关键字out有两层含义:

        子类型化会保留(Producer<Cat>是Producer<Animal>的子类型)

        T只能用在out位置 

        现在我们看看List<Interface>接口。Kotlin的List是只读的,所以它只有一个返回类型为T的元素的方法get,而没有定义任何把类型T的元素存储到列表中的方法。因此,它也是协变的。

        注意,类型形参不光可以直接当做参数类型或者返回类型使用,还可以当做另一个类型的类型实参。例如,List接口就包含了一个返回List<T>的subList方法。         在这个例子中,函数subList中的T也用在了out位置。这里我们不再深入细节。

        注意,不能把MutableList<T>在它的类型形参上声明成协变的,因为它既可以含有接收类型为T的值作为参数的方法,也含有返回这种值的方法(因此,T出现在in和out两种位置上)。

        编译器强制实施了这种限制。如果这个类被声明成协变的,编译器会报告错误         注意,构造方法的参数既不在in位置,也不在out位置。即使类型参数声明成了out,仍然可以在构造方法参数的声明中使用它

         如果把类的实例当成一个更泛化的类型的实例使用,变型会防止该实例被误用:不能调用存在潜在危险的方法。构造方法不是那种在实例创建之后还能调用的方法,因此它不会有潜在的风险

        然而,如果你在构造方法的参数上使用了关键字val和var,同时就会声明一个getter和一个setter(如果属性是可变的)。因此,对只读属性来说,类型参数用在了out位置,而可变熟悉在out和in位置都使用了它。

        上面这个例子中,T不能用out标记,因为类包含属性leadAnimal的setter,它在in位置用到了T。

        还要留意的是,位置规则只覆盖了类外部可见的(public、protected和internal)API。私有方法的参数既不在in位置也不在out位置。变型规则只会防止外部使用者对类的误用但不会对类自己的实现起作用。 

        现在可以安全地让Herd在T上协变,因为属性leadAnimal变成了私有。 

4、逆变:反转子类型化关系

        逆变的概念可以被看成是协变的镜像:对一个逆变类来说,它的子类型化关系与用作类型实参的类的子类型化关系是相反的。我们从Comparatpor接口的例子开始,这个接口定义了一个方法compare,用于比较两个给定的对象:         

        如你所见,这个接口方法只是消费类型为T 的值。这说明T只在in位置使用,因此它的声明之前使用了in关键字。

        一个为特定类型的值定义的比较器显然可以比较该类型任意子类型的值。例如,如果有一个Comparator<Any>,可以用它比较任意具体类型的值。         sortedWith函数期望一个Comparator<String>(一个可以比较字符串的比较器),传给它一个能比较更一般的类型的比较器是安全的。如果你要在特定类型的对象上执行比较,可以使用能处理该类型或者它的超类型的比较器。这说明Comparator<Any>是Comparator<String>的子类型,其中Any是String的超类型。不同类型之间的子类型关系和这些类型的比较器之间的子类型化关系截然相反。

         现在你已经为完整的逆变定义做好了准备。一个在类型参数上逆变的类是这样的一个泛型类(我们以Consumer<T>为例),对这种类来说,下面的描述是成立的:如果B是A的子类型,那么Consumer<A>就是Consumer<B>的子类型。类型参数A和B交换了位置,所以我们说子类型化被反转了。例如Consumer<Animal>是Consumer<Cat>的子类型。

         in关键字的意思是,对应类型的值是传递进来给这个类的方法的,并且被这个方法消费。和协变的情况类似,约束类型参数的使用将导致特定的子类型化关系。在类型参数T上的in关键字意味着子类型化被反转了,而且T只能用在in位置。下图总结了可能的变型选择之间的差异。

        一个类可以在一个类型参数上协变,同时在另一个类型参数上逆变。Function接口就是一个经典的例子。下面是一个单个参数的Function的声明: 

        kotlin的表示法(P) -> R是表达Function<P, R>的另一种更具可读性的形式。可以发现用in关键字标记的P(参数类型)只用在in位置,而用out关键字标记的R(返回类型)只用在了out位置。这意味着对这个函数类型的第一个类型参数来说,子类型化被反转了,而对第二个类型参数来说,子类型化保留了。例如,你有一个高阶函数,该函数尝试对你所有的猫进行迭代,你可以把一个接收任意动物的lambda传给它。

5、使用点变型:在类型出现的地方指定变型

        在类声明的时候就能够指定变型修饰符是很方便的,因为这些修饰符会应用到所有类被使用的地方。这被称作声明点变型如果你熟悉java的通配符类型(?extends和? super),你会意识到java用完全不同的方式处理变型。在java中,每一次使用带类型参数的类型的时候,还可以指定这个类型参数是否可以用它的子类型或者超类型替换。这叫做使用点变型

        kotlin也支持使用点变型,允许在类型参数出现的具体位置指定变型,即使在类型声明时它不能被声明成协变或逆变的。

        你已经见过许多像MutableList这样的接口,通常情况下既不是协变也不是逆变的,因为它同时生产和消费指定为它们类型参数的类型的值但是对于这个类型的变量来说,在某个特定函数只被当成其中一种角色使用的情况挺常见的:要么是生产者要么是消费者。         

        这个函数从一个集合把元素拷贝到另一个集合中。尽管两个集合都拥有不变型的类型,来源集合只是用于读取,而目标集合只是用于写入。 这种情况下,集合元素的类型不需要精确匹配。例如,把一个字符串的集合拷贝到可以包含任何对象的集合中一点儿问题都没有。

        要让这个函数支持不同类型的列表,可以引入第二个泛型参数。

        你声明了两个泛型参数代表来源列表和目标列表中的元素类型。为了能够把一个列表中的元素拷贝到另一个列表中,来源元素类型应该是目标列表中的元素的子类型

        但是kotlin提供了一种更优雅的表示方式当函数的实现调用了那些类型参数只出现在out位置(或只出现在in位置)的方法时,可以充分利用这一点,在函数定义中给特定用途的类型参数加上变型修饰符

        (个人理解:copyData函数的实现调用了source对象上的get方法,这个方法类型参数T只出现在out位置

         可以为类型声明中类型参数任意的用法指定变型修饰符,这些用法包括:形参类型、局部变量类型、函数返回类型等等。这里发生的一切被称作类型投影我们说source不是一个常规的MutableList,而是一个投影(受限)的MutableList。只能调用返回类型是泛型类型参数的那些方法,或者严格地讲,只在out位置使用它的方法。编译器禁止调用使用类型参数做实参(类型)的那些方法(在in位置使用类型参数)

        不要为使用投影类型后不能调用某些方法而吃惊。如果需要调用那些方法,你要用的是常规类型而不是投影。这可能要求你声明第二个类型参数,它依赖的是原本来进行投影的类型。就像代码清单9.15中那样。

        当然,实现copyData函数的正确方式应该是使用List<T>作为source实参的类型,因为我们只用了声明在List的方法,并没有使用MutableList中的方法,而且List类型参数的变型在声明时就指定了。但这个例子对展示这个概念依然十分重要,尤其是要记住大多数的类并没有像List和MutableList这样分开的两个接口,一个是协变的读取接口,另一个是不变形的读取/写入接口

        如果类型参数已经有out变型,获取它的out投影没有任何意义。就像List<out T>这样。它和List<T>是一个意思,因为List已经声明成了class List<out T>。kotlin编译器会发出警告,表明这样投影是多余的。

        同理,可以对类型参数的用法使用in修饰符,来表明在这个特定的地方,相应的值担当的是消费者,而且类型参数可以使用它的任意子类型替换。下面展示了如何使用in投影来重写上面代码:

        使用点变型有助于放宽可接收的类型的范围。现在我们来讨论一种极端情况:这种情况下(泛型)类型使用所有可能的类型的实参,都是可以接受的。 

6、星号投影:使用*代替类型参数

         本章前面提到类型检查和转换的时候,我们提到了一种特殊的星号投影语法。可以用它来表明你不知道关于泛型实参的任何信息。例如,一个包含未知类型的元素的列表用这种语法表示为List<*>。现在我们深入探讨星号投影的语义。

         首先,需要注意的是MutableList<*>和MutableList<Any?>不一样(这里非常重要的是MutableList<T>在T上是不变型的)。你确信MutableList<Any?>这种列表包含的是任意类型的元素。而另一方面,MutableList<*>是包含 某种特定类型元素的列表,比如String(你无法创建一个ArrayList<*>),而且创建它的代码期望只包含那种类型的元素。因为不知道是哪个类型,你不能向列表中写入任何东西,因为你写入的任何值都可能违反调用代码的期望。但是从列表中读取元素是可行的,因为你心里有数,所有的存储在列表中的值都能匹配所有Kotlin类型的超类型Any?:

        为什么编译器会把MutableList<*>当成out投影的类型?在这个例子的上下文中,MutableList<*>投影成了MutableList<out Any?>:当你没有任何元素类型信息的时候,读取Any?类型的元素仍然是安全的,但是向列表中写入元素是不安全的。谈到java通配符,kotlin的MyType<*>对应于java的MyType<?>。        当类型实参的信息并不重要的时候,可以使用星号投影的语法:不需要使用任何在签名中引用类型参数的方法,或者只是读取数据而不关心它的具体类型。例如,可以实现一个接收List<*>做参数的printFirst函数:

 

        星号投影的语法很简洁,但只能用在对泛型类型实参的确切值不感兴趣的地方:只是使用生产值的方法,而且不关心那些值的类型。 

         现在我们来看另外一个使用星号投影的类型的例子,以及使用这种方式时常见的会困扰你的陷阱。假设你需要验证用户的输入,并声明了一个接口FiledValidator。它只包含在in位置的类型参数,所以声明成了逆变。而且,事实上,当期望的是字符串验证器时使用可验证任意元素的验证器也是没有问题的(这正是把它声明成逆变带来的效果)。你还声明了两个验证器来分别处理String和Int。

        现在假设你想要把所有的验证器都存储在同一个容器中,并根据输入的类型来选出正确的验证器。你首先会想到使用map来存储他们。你要存储的是任意类型的验证器,所以你声明了KClass(代表一个kotlin类——第10章会详细介绍KClass)到FiledValidator<*>(可以指向任何类型的验证器)的map。         在前面尝试向MutableList<*>中写入元素的时候,你已经见过这个错误了。这种情况下,这个错误的意思是把具体类型的值传给未知类型的验证器是不安全的。一种修正的方法时把验证器显式地转换成需要的类型。这样做是不安全的,也是不推荐的。但我们还是把它作为让代码快速通过编译的技巧展示在这里,这样可以在后面来重构它。

        错误的代码和代码清单9.19中的代码在两种情境下是相似的,都只会发出警告。只转换那些类型正确的值是你的职责。

        这种解决办法不是类型安全的,而且容易出错。所以,需要想要把不同类型的验证器存储在同一个地方,我们来研究下其他的选择。

        下面代码的解决方法中使用了同样的map validators,但是把所有对它的访问封装到了两个泛型方法中,它们负责保证只有正确的验证器被注册和返回这段代码依然会发出未受检转换的警告(这之前的一样),但这里的Validators对象控制了所有对map的访问,保证了没有任何人会错误地改变map。

         现在你拥有了一个类型安全的API。所有不安全的逻辑都被隐藏在类的主体中,通过把这些逻辑局部化,保证了它不会被错误地使用。编译器禁止使用错误的验证器,因为Validators对象始终都会给出正确的验证器实现

        这种模式可以轻松地推广到任意自定义泛型类的存储。把不安全的代码局部化到一个分开的位置预防了误用,而且让容器的使用变得安全。注意这里描述的模式并不只是针对kotlin,在java中也可以使用同样的方法。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值