[读书日志]从零开始学习Chisel 第十一篇:Scala的类型参数化(敏捷硬件开发语言Chisel与数字系统设计)

devtools/2025/1/11 19:40:42/

8.Scala的类型参数化

8.1 var类型的字段

对于可重新赋值的字段,可以执行两个基本操作,获取字段值或设置为一个新值。如果在类中定义了一个var类型的字段,那么编译器会把这个变量限制为private[this],同时隐式地定义一个名为变量名getter方法和一个名为变量名_=setter方法。默认的getter方法返回变量的值,默认的setter方法接收外部传入的参数来直接赋值给变量。

scala">scala> import scala.compiletime.uninitializedscala> class A {| var aInt: Int = uninitialized| }
// defined class A

注意,原书中使用下划线赋初值的方式在Scala3中被废除,推荐使用这样的方式赋初值。

外部仍然可以读取和修改该字段,但编译器自动将其转换为对settergetter的调用。

scala">scala> class A {| private var a: Int = uninitialized| def originalValue: Int = a| def originalValue_=(x:Int) = a = x| def tenfoldValue: Int = a * 10| def tenfoldValue_= (x: Int) = a = x / 10| }
// defined class Ascala> val a = new A
val a: A = A@23a0c7f3scala> a.originalValue = 1scala> a.originalValue
val res32: Int = 1scala> a.tenfoldValue
val res33: Int = 10scala> a.tenfoldValue = 1000scala> a.tenfoldValue
val res34: Int = 1000scala> a.originalValue
val res35: Int = 100

在Scala3中不推荐使用private[this],只需要使用private即可。我们可以自定义这两个方法来操作

在这个例子中我们重写了这个变量的方法,这两组方法都会修改这个变量的值。

8.2 类型构造器
scala">scala> abstract class A[T] {| val a: T| }
// defined class A

这个例子中A被称为类型构造器,它接收一个类型参数来构造一个类型,也可以说A是一个泛型的类。

scala">scala> def doesCompile(x: A[AnyRef]) = {}
def doesCompile(x: A[AnyRef]): Unit

泛型的类和特质需要在方括号中加上类型参数,如果某个成员方法的参数也是泛型的,那么方法名后面也必须加上方括号和类型参数,字段则不需要。

8.3 型变注解

A[T]这样的类型构造器,它们的类型参数可以是协变的,逆变的或者不变的。这被称为类型参数的型变。A[+T]表示类A在类型参数T上是协变的,A[-T]表示逆变,如果没有就是不变的,+和-被称为型变注解。

在这里插入图片描述

如果类型Sub是类型Super的子类,那么协变表示Tmep[Sub]也是Temp[Super]的子类型,逆变表示Temp[Sub]Temp[Super]的超类,不变则表示这两个是两种没有任何关系的不同类型。

8.4 检查型变注解

标记了型变注解的类型参数不能随便使用,类型系统设计要满足里氏替换原则,即在任何需要类型为T的对象的地方,都能用T的子类的对象替换。

假设类型T类型S的超类,如果类型参数是协变的,导致A[T]也是A[S]的超类,那么val a : A[T]= new A[S]就合法。此时,如果类A内部的某个方法funcA的入参的类型也是这个协变类型参数,那么方法调用a.funcA(b:T)就会出错,因为a实际指向的是一个子类对象,子类对象的方法funcA接收的入参的类型是S,而子类S不能指向超类T,所以传入的b不能被接收。但是a的类型是A[T]又隐式地告诉使用者,可以传入类型是T的参数,这就产生了矛盾。相反,funca的返回类型是协变类型参数就没有问题,因为子类对象的funcA的返回值的类型虽然是S,但是能被T类型的变量接收,即val c : T = a.funcA()合法。a的类型A[T]隐式地告诉使用者应该用T类型的变量接收返回值,虽然实际返回的值是S类型,但是子类型多态允许这样做。

在这里插入图片描述

要保证不出错,生产者产生的值的类型应该是子类,消费者接收的值的类型应该是超类(接收者本来只希望使用超类的成员,但是实际给出的子类统统都具备,接收者也不会去使用多出来的成员,所以子类型多态才正确)。

基于此,方法的入参的类型应该是逆变类型参数,逆变使得val a : A[S]= newA[T]合法,也就是实际引用的对象的方法想要一个T类型的参数,但传入了子类型S的值,符合里氏替换原则。同理,方法的返回类型应该是协变的

既然类型参数的使用有限制,那么就应该有一条规则来判断该使用什么类型参数。 Scala 的编译器把类或特质中任何出现类型参数的地方都当作一个"点",点有协变点、逆变点和不变点之分,以声明类型参数的类和特质作为顶层开始,逐步往内层深入,对这些点进行归类。

在顶层的点都是协变点,例如,顶层的方法的返回类型就在协变点。默认情况下,在更深一层的嵌套的点与在包含嵌套的外一层的点被归为一类。

该规则有一些例外:

①方法的值参数所在的点会根据方法外的点进行一次翻转,也就是把协变点翻转成逆变点、逆变点翻转成协变点、不变点仍然保持不变;

②方法的类型参数(即方法名后面的方括号)也会根据方法外的点进行一次翻转;

③如果类型也是一个类型构造器,比如以C[T]为类型,那么,当 t 有"-“注解时就根据外层进行翻转,有”+"注解时就保持与外层一致,否则就变成不变点。

协变点只能用"+“注解类型参数,逆变点只能用”-"注解类型参数。没有型变注解的类型参数可以用在任何点,也是唯一能用在不变点的类型参数。所以对于类型Q[+U,-T,V]而言,U处在协变点,T处在逆变点,而V处在不变点。以如下例子为例进行解释:

scala">scala> abstract class Cat[-T, +U] {|   def meow[W-](volume: T-, listener: Cat[U+, T-]-): Cat[Cat[U+, T-]-, U+]+| }
// defined class Cat

这个例子中的正号表示协变点,负号表示逆变点。首先,Cat类声明了类型参数,所以它是顶层。方法meow的返回值属于顶层的点,所以返回类型的最右边是正号,表示协变点。因为方法的返回类型也是类型构造器Cat,并且第一个类型参数是逆变的,所以这里相对协变翻转成了逆变,而第二个类型参数是协变的,所以保持协变属性不变。继续往里归类,返回类型嵌套的 Cat 处在逆变点,所以第一个类型参数的位置相对逆变翻转成协变,第二个类型参数的位置保持逆变属性不变。两个值参数volumelistener都相对协变翻转成了逆变,并转成了逆变。且listener的类型是Cat,所以和返回类型嵌套的Cat一样。方法的类型参数W也相对协变翻虽然型变注解的检查很复杂,但这些工作都被编译器自动完成了。编译器的检查方法也很直接,就是查看顶层声明的类型参数是否出现在正确的位置。比如,上例中,T都出现在逆变点,U都出现在协变点,W是什么并不关心。

8.5 类型构造器的继承关系

因为类型构造器需要根据类型参数来确定最终的类型,所以在判断多个类型构造器之间的继承关系时,必须依赖类型参数。对于只含单个类型参数的类型构造器而言,继承关系很好判断,只需要看型变注解是协变、逆变还是不变。当类型参数不止一个时,该如何判断呢?尤其是当函数的参数是一个函数时,更需要确定一个函数的子类型是什么样的函数。以常用的单参数函数为例,其特质Function1的部分定义如下:

scala">scala> trait Function1[-S, +T] {| def apply(x: S): T| }
// defined trait Function1

类型参数 S 代表函数的入参的类型,应该是逆变的。类型参数 T 代表函数返回值的类型,所以是协变的。

在这里插入图片描述

假设类A类a的超类,类B类b的超类,并且定义了一个函数的类型为Function1[a,B],那么,这个函数的子类型应该是Function1[A,b]。解释如下:假设在需要类型为Function1[a,B]的函数的地方,实际用类型为Functionl[A,b]的函数代替了。那么,本来会给函数传入a类型的参数,但实际函数需要A类型的参数,因为类A类a的超类,这符合里氏替换原则;本来会用类型为B的变量接收函数的返回值,但实际函数返回了b类型的值,因为类B类b的超类,这也符合里氏替换原则。综上所述,用Function1[A,b]代替Function1[a,B]符合里氏替换原则,所以Function1[A,b]Function1[a,B]的子类型。

因此,对于含有多个类型参数的类型构造器,要构造子类型,就是把逆变类型参数由子类替换成超类、把协变类型参数由超类替换成子类。

8.6 上界和下界

对于类型构造器A[+T],倘若没有别的手段,很显然它的方法的参数不能泛化,因为协变的类型参数不能用作函数的入参类型。如果要泛化参数,必须借助额外的类型参数,那么这个类型参数该怎么定义呢?因为可能存在val x : A[超类]= new A[子类]这样的定义,导致方法的入参类型会是T的超类,所以,额外的类型参数必须是T的超类。Scala 提供了一个语法——下界,其形式为U >: T,表示U必须是T的超类,或者是T本身(一个类型既是它自身的超类,又是它自身的子类)。 通过使用下界标定一个新的类型参数,就可以在A[+T]这样的类型构造器中泛化方法的返回入参类型。例如:

scala">scala> abstract class A[+T] {| def funcA[U >: T](x: U): U| }
// defined class A

现在,编译器不会报错,下界的存在导致编译器预期参数x的类型是T的超类。实际运行时,会根据传入的实际入参确定U是什么。返回类型定义成了U,当然也可以是T,但是动态地根据U来调整类型显得更自然。

与下界对应的是上界,形式为U <: T,表示U必须是T的子类或本身。通过上界,可以在A[-T]这样的类型构造器中泛化方法的返回类型:

scala">scala> abstract class A[-T] {| def funcA[U <: T](x: U): U| }
// defined class A
8.7 方法的类型参数

除了类和特质可以声明类型参数,方法也可以带有类型参数。如果方法仅仅使用了包含它的类或特质已声明的类型参数,那就不用自己写出类型参数。如果出现了未声明的类型参数,则必须写在方法的类型参数中。方法的类型参数不能有型变注解。

scala">scala> abstract class A[-T] {| def funcA(x: T): Unit| }
// defined class Ascala> abstract class A[-T] {| def funcA[U](x: T, y: U): Unit| }
// defined class A

方法的类型参数不能与包含它的类和特质已声明的类型参数一致,否则会将它们覆盖。

8.8 对象私有数据

var类型的字段,其类型参数不能是协变的或逆变的。

如果var字段是对象私有的,即用private修饰,那么它只能在定义该类或特质时被访问。由于外部无法访问,因此可以忽略对型变注解的检查。


http://www.ppmy.cn/devtools/149683.html

相关文章

npm run 运行项目报错:Cannot resolve the ‘pnmp‘ package manager

尝试使用 npm 运行一个项目&#xff0c;但是在解析 pnmp 包管理器时遇到了问题。这通常意味着项目可能配置错误&#xff0c;或者可能误输入了命令。 解决方法&#xff1a; 确认是否有拼写错误。通常情况下&#xff0c;应该是 npm 而不是 pnmp。 检查项目的 package.json 文件&…

安装rocketmq dashboard

1、访问如下地址&#xff1a; GitHub - apache/rocketmq-dashboard: The state-of-the-art Dashboard of Apache RoccketMQ provides excellent monitoring capability. Various graphs and statistics of events, performance and system information of clients and applica…

从excel提取和过滤数据到echarts中绘制图

主页面 介绍 echarts的事例页面,导入数据比较麻烦,此项目从excel中提取数据(含过滤数据),以注入页面. 代码说明 所有的需要从excel中读取的参数,从代码中替换.需以{{data}} 包含在内使用绘制参数的解析代码参数可以解析出来所有参数数据配置上传文件后,可以选择列数据过滤条…

Win10微调大语言模型ChatGLM2-6B

在《Win10本地部署大语言模型ChatGLM2-6B-CSDN博客》基础上进行&#xff0c;官方文档在这里&#xff0c;参考了这篇文章 首先确保ChatGLM2-6B下的有ptuning AdvertiseGen下载地址1&#xff0c;地址2&#xff0c;文件中数据留几行 模型文件下载地址 &#xff08;注意&#xff1…

高级软件工程-复习

高级软件工程复习 坐标国科大&#xff0c;下面是老师说的考试重点。 Ruby编程语言的一些特征需要了解要能读得懂Ruby程序Git的基本命令操作知道Rails的MVC工作机理需要清楚&#xff0c;Model, Controller, View各司什么职责明白BDD的User Story需要会写&#xff0c;SMART要求能…

Stable diffusion的SDXL模型,针不错!(含实操)

与之前的SD1.5大模型不同&#xff0c;这次的SDXL在架构上采用了“两步走”的生图方式&#xff1a; 以往SD1.5大模型&#xff0c;生成步骤为 Prompt → Base → Image&#xff0c;比较简单直接&#xff1b;而这次的SDXL大模型则是在中间加了一步 Refiner。Refiner的作用是什么呢…

2025年01月09日Github流行趋势

1. 项目名称&#xff1a;khoj 项目地址url&#xff1a;https://github.com/khoj-ai/khoj项目语言&#xff1a;Python历史star数&#xff1a;22750今日star数&#xff1a;1272项目维护者&#xff1a;debanjum, sabaimran, MythicalCow, aam-at, eltociear项目简介&#xff1a;你…

网站自动签到

我研究生生涯面临两个问题&#xff0c;一是写毕业论文&#xff0c;二是找工作&#xff0c;这两者又有很大的冲突。怎么解决这两个冲突呢&#xff1f;把python学好是一个路子&#xff0c;因此从今天我要开一个专栏就是学python 其实我的本意不是网站签到&#xff0c;我喜欢在起点…