介绍Ceylon语言第6部分

   |       Ceylon

这是介绍Ceylon语言的系列文章的第6篇。请注意,语言的一些功能在最终发布前可能会发生变化。

定义泛型类型

在这个系列的文章中,我们已经看到了许多参数化类型,但现在让我们进一步探讨一些细节。

使用泛型类型编程是Java中最困难的部分之一。这在某种程度上对Ceylon也是成立的。但由于Ceylon语言和SDK是从底层为泛型设计的,因此Ceylon能够减轻Java后期附加模型的许多痛苦方面。

就像在Java中一样,只有类型和方法可以声明类型参数。同样,在Java中,类型参数在普通参数之前列出,用尖括号括起来。

shared interface Iterator<out Element> { ... }
class Array<Element>(Element... elements) satisfies Sequence<Element> { ... }
shared Entries<Natural,Value> entries<Value>(Value... sequence) { ... }

如您所见,Ceylon的惯例是使用有意义的名称来为类型参数命名。

与Java不同,我们始终需要在类型声明中指定类型参数(Ceylon中没有原始类型)。以下代码将无法编译

Iterator it = ...;   //error: missing type argument to parameter Element of Iterator

我们始终需要在类型声明中指定一个类型参数

Iterator<String> it = ...;

另一方面,我们通常不需要在大多数方法调用或类实例化中显式指定类型参数。原则上,通常可以从普通参数推断出类型参数。以下代码应该是可能的,就像它在Java中一样

Array<String> strings = Array("Hello", "World");
Entries<Natural,String> entries = entries(strings);

但我们还没有确定类型推断算法的确切内容(可能涉及到联合类型!),因此Ceylon编译器目前要求显式指定所有类型参数,如下所示

Array<String> strings = Array<String>("Hello", "World");
Entries<Natural,String> entries = entries<Natural,String>(strings);

另一方面,以下代码已经可以编译

local strings = Array<String>("Hello", "World");
local entries = entries<Natural,String>(strings);

在Java中处理泛型类型时遇到的大多数问题的根本原因在于 类型擦除。泛型类型参数和参数被编译器丢弃,在运行时根本不可用。因此,以下完全合理的代码片段在Java中是无法编译的

if (is List<Person> list) { ... }
if (is Element obj) { ... }

(其中Element是一个通用的类型参数。

Ceylon类型系统的一个主要目标是支持具体化泛型。与Java类似,Ceylon编译器执行擦除操作,从泛型类型的模式中丢弃类型参数。但是与Java不同,类型参数应该被具体化(在运行时可用)。当然,泛型类型参数在运行时不会被底层虚拟机检查类型安全性,但至少类型参数对需要显式使用它们的代码在运行时是可用的。因此,上述代码片段应该能够编译并按预期运行。你甚至可以使用反射来发现泛型类型的实例的类型参数。

坏消息是我们还没有实现这一点;-)

最后,Ceylon消除了Java泛型中很难理解的部分之一:通配符类型。通配符类型是Java解决泛型类型系统中协变问题的一种方法。让我们首先探索协变的概念,然后看看Ceylon中的协变是如何工作的。

协变和逆变

一切始于对集合的直观预期:Geek是一个集合的Person。这是一个合理的直觉,但在非函数式语言中,尤其是在集合可变的语言中,这实际上是错误的。考虑以下可能的Collection:

shared interface Collection<Element> { 
    shared formal Iterator<Element> iterator(); 
    shared formal void add(Element x);
}

定义,假设GeekPerson的子类型。

直观的预期是以下代码应该可以工作:

Collection<Geek> geeks = ... ; 
Collection<Person> people = geeks;    //compiler error 
for (Person person in people) { ... }

这段代码,坦白说,从字面上看是完全可以接受的。然而,在Java和Ceylon中,这段代码在第二行产生了编译错误,即将Collection分配给一个Collection。为什么?因为如果我们允许这个赋值通过,以下代码也将编译:

Collection<Geek> geeks = ... ; 
Collection<Person> people = geeks;    //compiler error 
people.add( Person("Fonzie") );

我们不能允许这样——Fonzie不是一个人。Geek!

用大词说,我们说CollectionElement中是非变异的。Collection或者,当我们不想用模糊的术语来给人留下印象时,我们可以说通过iterator()方法产生,并通过add()Element.

方法消耗——类型

这是Java跳入兔子洞的地方,成功地使用通配符从非变异类型中挤压出协变或逆变类型,但同时也成功地彻底混淆了所有人。我们不打算跟随Java跳入洞中。Collection相反,我们将重构

shared interface Producer<out Output> { 
    shared formal Iterator<Output> iterator();
}
shared interface Consumer<in Input> { 
    shared formal void add(Input x);
}

为纯生产者接口和纯消费者接口

  • 请注意,我们已经注解了这些接口的类型参数。The注解指定ProducerOutput上是协变的;它产生Output的实例,但从不消耗Output.
  • 请注意,我们已经注解了这些接口的类型参数。注解指定Consumer是逆变在Input;它消耗Input的实例,但从不产生Input.

的实例方法产生,并通过Ceylon编译器验证类型声明的模式,以确保满足变异性注解。如果你尝试在Producer上声明一个iterate()Ceylon编译器验证类型声明的模式,以确保满足变异性注解。如果你尝试在Consumer方法,则会导致编译错误。

现在,让我们看看这能给我们带来什么

  • 由于Producer在它的类型参数上是协变的,并且由于Output,Ceylon允许你将GeekPersonProducer分配给Producer此外,由于.
  • 在它的类型参数上是逆变的ConsumerConsumerInput,Ceylon允许你将GeekPersonProducerConsumerProducer我们可以将我们的.

接口定义为CollectionProducer的混合Consumer.

shared interface Collection<Element> 
        satisfies Producer<Element> & Consumer<Element> {}

请注意,CollectionElement中是非变异的。如果我们尝试向ElementCollection添加变异性注解,则会引发编译时错误。

现在,以下代码最终可以编译

Collection<Geek> geeks = ... ; 
Producer<Person> people = geeks; 
for (Person person in people) { ... }

这与我们的原始直觉相符。

以下代码也可以编译

Collection<Person> people = ... ; 
Consumer<Geek> geekConsumer = people; 
geekConsumer.add( Geek("James") );

这也是直观正确的——詹姆斯确实是一个Person!

从协方差和逆协方差定义中可以得出两个附加事项

  • Producer<Void>Producer<T>的任何类型T的父类型,并且
  • Consumer<Bottom>Consumer<T>的任何类型T.

这些不变量在需要抽象处理所有Producer或者所有Consumer时非常有用。(然而,请注意,如果ProducerOutput上声明了上界类型约束,则Producer<Void>将不是一个合法的类型。)

您不太可能花很多时间编写自己的集合类,因为Ceylon SDK内置了一个强大的集合框架。但作为内置集合类型的用户,您仍会欣赏Ceylon对协方差的处理方法。集合框架为每种基本类型的集合定义了两个接口。例如,有一个接口List<Element>它表示列表的只读视图,并在Element的父类型,并且OpenList<Element>中协变,它表示可变列表,并且在Element.

泛型类型约束

非常常见,当我们编写参数化类型时,我们希望能够在类型参数的实例上调用方法或评估属性。例如,如果我们正在编写参数化类型Set<Element>,我们则需要能够使用Element来比较==的实例,以查看某个实例是否包含在ElementSet中。由于仅定义在类型为==Equality的表达式上,我们需要某种方式来断言。这是一个类型约束的例子——事实上,它是最常见类型约束的例子之一,即上界。Element的表达式上,我们需要某种方式来断言A type argument to

shared class Set<out Element>(Element... elements) 
        given Element satisfies Equality {
    ...

    shared Boolean contains(Object obj) { 
        if (is Element obj) {
            return obj in bucket(obj.hash);
        }
        else {
            return false;
        }
    }

}

必须是一个子类型Elementof的表达式上,我们需要某种方式来断言.

Set<String> set = Set("C", "Java", "Ceylon"); //ok
Set<String?> set = Set("C", "Java", "Ceylon", null); //compile error

In Ceylon,a generic type parameter is considered a proper type,so a type constraint looks a lot like a class or interface declaration. This is another way in which Ceylon is more regular than some other C-like languages.

An upper bound lets us call methods and attributes of the bound,but it doesn't let us instantiate new instances ofElement. Once we implement reified generics,we'll be able to add a new kind of type constraint to Ceylon. An initialization parameter specification lets us actually instantiate the type parameter.

shared class Factory<out Result>() 
        given Result(String s) {

    shared Result produce(String string) { 
        return Result(string);
    }

}

必须是一个子类型ResultofFactorymust be a class with a single initialization parameter of typeString.

Factory<Hello> = Factory<PersonalizedHello>(); //ok
Factory<Hello> = Factory<DefaultHello>(); //compile error

A third kind of type constraint is an enumerated type bound,which constrains the type argument to be one of an enumerated list of types. It lets us write an exhaustiveswitchon the type parameter

Value sqrt<Value>(Value x) 
        given Value of Float | Decimal {
    switch (Value)
    case (satisfies Float) {
        return sqrtFloat(x);
    } 
    case (satisfies Decimal) {
        return sqrtDecimal(x);
    }
}

This is one of the workarounds we mentioned earlier for Ceylon's lack of overloading.

Finally,the fourth kind of type constraint,which is much less common,and which most people find much more confusing,is a lower bound. A lower bound is the opposite of an upper bound. It says that a type parameter is a supertype of some other type. There's only really one situation where this is useful. Consider adding aunion()operation to our中。由于interface. We might try the following

shared class Set<out Element>(Element... elements) 
        given Element satisfies Equality {
    ...
    
    shared Set<Element> union(Set<Element> set) {   //compile error
        return ....
    }
    
}

This doesn't compile because we can't use the covariant type parameterTin the type declaration of a method parameter. The following declaration would compile

shared class Set<out Element>(Element... elements) 
        given Element satisfies Equality {
    ...
    
    shared Set<Object> union(Set<Object> set) { 
        return ....
    }
    
}

But, unfortunately, we get back aSet<Object>no matter what kind of set we pass in. A lower bound is the solution to our dilemma

shared class Set<out Element>(Element... elements) 
        given Element satisfies Equality {
    ...
    
    shared Set<UnionElement> union(Set<UnionElement> set) 
            given UnionElement abstracts Element {
        return ...
    }
    
}

With type inference,the compiler chooses an appropriate type argument toUnionElement对于给定的参数union():

Set<String> strings = Set("abc", "xyz") ; 
Set<String> moreStrings = Set("foo", "bar", "baz"); 
Set<String> allTheStrings = strings.union(moreStrings);
Set<Decimal> decimals = Set(1.2.decimal, 3.67.decimal) ; 
Set<Float> floats = Set(0.33, 22.0, 6.4); 
Set<Number> allTheNumbers = decimals.union(floats);
Set<Hello> hellos = Set( DefaultHello(), PersonalizedHello(name) ); 
Set<Object> objects = Set("Gavin", 12, true); 
Set<Object> allTheObjects = hellos.union(objects);

还有更多...

我本来要开始谈论序列化类型参数,这是Ceylon类型安全元模型的基础。但我意识到我已经达到了字数限制。如果你真的很不耐烦,可以跳到第8部分

在第7部分中,我们将回顾一下,并涵盖一些被略过的话题。


返回顶部