Kotlin笔记(六):泛型的高级特性

  前面学习了Kotlin中的泛型的基本用法,跟Java中的泛型大致相同,Kotlin在泛型方面还提供了不少特有的功能,掌握了这些功能,你将可以更好玩转Kotlin,同时还能实现一些不可思议的语法特性,那么我们自然不能错过这部分内容了。

1. 对泛型进行实化

  Java中完全没有泛型实化这个概念。而如果我们想要深刻地理解泛型实化,就要先解释一下Java的泛型擦除机制才行。

 在JDK 1.5中,Java终于引入了泛型功能。但是实际上,Java的泛型功能是通过类型擦除机制来实现的。就是说泛型对于类型的约束只在编译时期存在,运行的时候仍然会按照JDK 1.5之前的机制来运行,JVM是识别不出来我们在代码中指定的泛型类型的。例如,假设我们创建了一List< String>集合,虽然在编译时期只能向集合中添加字符串类型的元素,但是在运行时期JVM并不能知道它本来只打算包含哪种类型的元素,只能识别出来它是个List。

 所有基于JVM的语言,它们的泛型功能都是通过类型擦除机制来实现的,其中当然也包括了Kotlin。这种机制使得我们不可能使用a is T或者T::class.java这样的语法,因为T的实际类型在运行的时候已经被擦除了。

 然而不同的是,Kotlin提供了一个内联函数的概念。内联函数中的代码会在编译的时候自动被替换到调用它的地方,这样的话也就不存在什么泛型擦除的问题了,因为代码在编译之后会直接使用实际的类型来替代内联函数中的泛型声明,其工作原理如图所示。
在这里插入图片描述

 可以看到,bar()是一个带有泛型类型的内联函数,foo()函数调用了bar()函数,在代码编译之后,bar()函数中的代码将可以获得泛型的实际类型。这就意味着,Kotlin中是可以将内联函数中的泛型进行实化的。

 那么具体该怎么写才能将泛型实化呢?首先,该函数必须是内联函数才行,也就是要用inline关键字来修饰该函数。其次,在声明泛型的地方必须加上reified关键字来表示该泛型要进行实化。示例代码如下:

inline fun <reified T> getGenericType() {
}

 上述函数中的泛型T就是一个被实化的泛型,因为它满足了内联函数和reified关键字这两个前提条件。那么借助泛型实化,到底可以实现什么样的效果呢?从函数名就可以看出来了,这里我们准备实现一个获取泛型实际类型的功能,代码如下所示:

inline fun <reified T> getGenericType() = T::class.java

 虽然只有一行代码,但是这里却实现了一个Java中完全不可能实现的功能:getGenericType()函数直接返回了当前指定泛型的实际类型。T.class这样的语法在Java中是不合法的,而在Kotlin中,借助泛型实化功能就可以使用T::class.java这样的语法了。

2. 泛型实化的应用

 泛型实化功能允许我们在泛型函数当中获得泛型的实际类型,这也就使得类似于a is T、T::class.java这样的语法成为了可能。而灵活运用这一特性将可以实现一些不可思议的语法结构.

 比如说启动一个Activity就可以这么写:

val intent = Intent(context, TestActivity::class.java)
context.startActivity(intent)

 通过Kotlin的泛型实化特性,我们可以进一步进行优化. 新建一个reified.kt文件,然后在里面编写如下代码:

inline fun <reified T> startActivity(context: Context) {val intent = Intent(context, T::class.java)context.startActivity(intent)
}

 Intent接收的第二个参数本来应该是一个具体Activity的Class类型,但由于现在T已经是一个被实化的泛型了,因此这里我们可以直接传入T::class.java。最后调用Context的startActivity()方法来完成Activity的启动。现在,如果我们想要启动TestActivity,只需要这样写就可以了:

startActivity<TestActivity>(context)

 通常在启用Activity的时候还可能会使用Intent附带一些参数, 而经过刚才的封装之后,我们就无法进行传参了。这个问题也不难解决,只需要借助高阶函数就可以轻松搞定。回到reified.kt文件当中,这里添加一个新的startActivity()函数重载,如下所示:

inline fun <reified T> startActivity(context: Context, block: Intent.() -> Unit) {val intent = Intent(context, T::class.java)intent.block()context.startActivity(intent)
}

 这样调用startActivity()函数的时候就可以在Lambda表达式中为Intent传递参数了,如下所示:

startActivity<TestActivity>(context) {putExtra("param1", "data")putExtra("param2", 123)
}

3. 泛型的协变

 泛型的协变和逆变功能不常用,但是在 Kotlin的内置API中用了很多协变和逆变的特性,所以我们可以了解一下,以便更深入的理解Kotlin.

 在开始学习协变和逆变之前,我们还得先了解一个约定。一个泛型类或者泛型接口中的方法,
它的参数列表是接收数据的地方,因此可以称它为in位置,而它的返回值是输出数据的地方,因
此可以称它为out位置,如图所示。
在这里插入图片描述
 先定义了一个Person类,类中包含name和age这两个字段。然后又定义了Student和
Teacher这两个类,让它们成为Person类的子类。

open class Person(val name: String, val age: Int)
class Student(name: String, age: Int) : Person(name, age)
class Teacher(name: String, age: Int) : Person(name, age)

 如果某个方法接收一个Person类型的参数,而我们传入一个Student的实例,这样合不合法呢?很显然,因为Student是Person的子类,学生也是人呀,因此这是一定合法的。

 再来升级一下这个问题:如果某个方法接收一个List< Person>类型的参数,而我们传入一个List< Student>的实例,这样合不合法呢?看上去好像也挺正确的,但是Java中是不允许这么做的,因为List< Student>不能成为List< Person>的子类,否则将可能存在类型转换的安全隐患。

 为什么会存在类型转换的安全隐患呢?下面我们通过一个具体的例子进行说明。这里自定义一
个SimpleData类,代码如下所示:

class SimpleData<T> {private var data: T? = nullfun set(t: T?) {data = t}fun get(): T? {return data}
}

 接着我们假设,如果编程语言允许向某个接收SimpleData< Person>参数的方法传入
SimpleData< Student>的实例,那么如下代码就会是合法的:

fun main() {val student = Student("Tom", 19)val data = SimpleData<Student>()data.set(student)handleSimpleData(data) // 实际上这行代码会报错,这里假设它能编译通过val studentData = data.get()
}
fun handleSimpleData(data: SimpleData<Person>) {val teacher = Teacher("Jack", 35)data.set(teacher)
}

 发现这段代码有什么问题吗?在main()方法中,我们创建了一个Student的实例,并将它封装到SimpleData< Student>当中,然后将SimpleData< Student>作为参数传递给handleSimpleData()方法。但是handleSimpleData()方法接收的是一个SimpleData< Person>参数(这里假设可以编译通过),那么在handleSimpleData()方法中,我们就可以创建一个Teacher的实例,并用它来替换SimpleData< Person>参数中的原有数据。这种操作肯定是合法的,因为Teacher也是Person的子类,所以可以很安全地将Teacher的实例设置进去。

 但是问题马上来了,回到main()方法当中,我们调用SimpleData< Student>的get()方法来获取它内部封装的Student数据,可现在SimpleData< Student>中实际包含的却是一个Teacher的实例,那么此时必然会产生类型转换异常。所以,为了杜绝这种安全隐患,Java是不允许使用这种方式来传递参数的。换句话说,即使Student是Person的子类,SimpleData< Student>并不是SimpleData< Person>的子类。

 不过,回顾一下刚才的代码,你会发现问题发生的主要原因是我们在handleSimpleData()方法中向SimpleData< Person>里设置了一个Teacher的实例。如果SimpleData在泛型T上是只读的话,肯定就没有类型转换的安全隐患了,那么这个时候SimpleData< Student>可不可以成为SimpleData< Person>的子类呢?讲到这里,我们终于要引出泛型协变的定义了。假如定义了一个MyClass< T>的泛型类,其中A是B的子类型,同时MyClass< A>又是MyClass< B>的子类型,那么我们就可以称MyClass在T这个泛型上是协变的。

即A,B的继承关系与其对应的泛型类的继承关系是一致的,即为协变.

 如何才能让MyClass< A>成为MyClass< B>的子类型呢?刚才已经讲了,如果一个泛型类在其泛型类型的数据上是只读的话,那么它是没有类型转换安全隐患的。而要实现这一点,则需要让MyClass< T>类中的所有方法都不能接收T类型的参数。换句话说,T只能出现在out位置上,而不能出现在in位置上。

 修改SimpleData类的代码,如下所示:

class SimpleData<out T>(val data: T?) {fun get(): T? {return data}
}

 这里我们对SimpleData类进行了改造,在泛型T的声明前面加上了一个out关键字。这就意味着现在T只能出现在out位置上,而不能出现在in位置上,同时也意味着SimpleData在泛型T上是协变的。

 由于泛型T不能出现在in位置上,因此我们也就不能使用set()方法为data参数赋值了,所以这里改成了使用构造函数的方式来赋值。你可能会说,构造函数中的泛型T不也是在in位置上的吗?没错但是由于这里我们使用了val关键字,所以构造函数中的泛型T仍然是只读的,因此这样写是合法且安全的。另外,即使我们使用了var关键字,但只要给它加上private修饰符,保证这个泛型T对于外部而言是不可修改的,那么就都是合法的写法。关键点就在于保证其只读性.

 经过了这样的修改之后,下面的代码就可以完美编译通过且没有任何安全隐患了:

fun main() {val student = Student("Tom", 19)val data = SimpleData<Student>(student)handleMyData(data)val studentData = data.get()
}
fun handleMyData(data: SimpleData<Person>) {val personData = data.get()
}

 由于SimpleData类已经进行了协变声明,那么SimpleData< Student>自然就是
SimpleData< Person>的子类了,所以这里可以安全地向handleMyData()方法中传递参数。

 然后在handleMyData()方法中去获取SimpleData封装的数据,虽然这里泛型声明的是Person类型,实际获得的会是一个Student的实例,但由于Person是Student的父类,向上转型是完全安全的,所以这段代码没有任何问题。

 学到这里,关于协变的内容就掌握得差不多了,不过最后还有个例子需要回顾一下。前面我们提到,如果某个方法接收一个List< Person>类型的参数,而传入的却是一个List< Student>的实例, 在Java中是不允许这么做的。注意这里我的用语,在Java中是不允许这么做的。

 你没有猜错,在Kotlin中这么做是合法的,因为Kotlin已经默认给许多内置的API加上了协变声明,其中就包括了各种集合的类与接口。Kotlin中的List本身就是只读的,如果你想要给List添加数据,需要使用MutableList才行。既然List是只读的,也就意味着它天然就是可以协变的,我们来看一下List简化版的源码:

public interface List<out E> : Collection<E> {override val size: Intoverride fun isEmpty(): Booleanoverride fun contains(element: @UnsafeVariance E): Booleanoverride fun iterator(): Iterator<E>public operator fun get(index: Int): E
}

 List在泛型E的前面加上了out关键字,说明List在泛型E上是协变的。不过这里还有一点需要说明,原则上在声明了协变之后,泛型E就只能出现在out位置上,可是你会发现,在contains()方法中,泛型E仍然出现在了in位置上。

 这么写本身是不合法的,因为在in位置上出现了泛型E就意味着会有类型转换的安全隐患。但是
contains()方法的目的非常明确,它只是为了判断当前集合中是否包含参数中传入的这个元素,而并不会修改当前集合中的内容,因此这种操作实质上又是安全的。那么为了让编译器能够理解我们的这种操作是安全的,这里在泛型E的前面又加上了一个@UnsafeVariance注解,这样编译器就会允许泛型E出现在in位置上了。但是如果你滥用这个功能,导致运行时出现了类型转换异常,Kotlin对此是不负责的。

4. 泛型的逆变

 理解了协变之后再来学习逆变会相对比较容易一些,因为它们之间是有所关联的。

 不过仅从定义上来看,逆变与协变却完全相反。那么这里先引出定义吧,假如定义了一个
MyClass< T>的泛型类,其中A是B的子类型,同时MyClass< B>又是MyClass< A>的子类型,
那么我们就可以称MyClass在T这个泛型上是逆变的。协变和逆变的区别如图所示。
在这里插入图片描述

 从直观的角度上来思考,逆变的规则好像挺奇怪的,原本A是B的子类型,怎么MyClass< B>能
反过来成为MyClass< A>的子类型了呢?下面我们通过一个具体的例子来学习一下。

 这里先定义一个Transformer接口,用于执行一些转换操作,代码如下所示:

interface Transformer<T> {fun transform(t: T): String
}

 Transformer接口中声明了一个transform()方法,它接收一个T类型的参数,并且返回一个String类型的数据,这意味着参数T在经过transform()方法的转换之后将会变成一个字符串。至于具体的转换逻辑是什么样的,则由子类去实现,Transformer接口对此并不关心。

 现在我们就尝试对Transformer接口进行实现,代码如下所示:

fun main() {val trans = object : Transformer<Person> {override fun transform(t: Person): String {return "${t.name} ${t.age}"}}handleTransformer(trans) // 这行代码会报错
}
fun handleTransformer(trans: Transformer<Student>) {val student = Student("Tom", 19)val result = trans.transform(student)
}

 首先我们在main()方法中编写了一个Transformer< Person>的匿名类实现,并通过transform()方法将传入的Person对象转换成了一个“姓名+年龄”拼接的字符串。而handleTransformer()方法接收的是一个Transformer< Student>类型的参数,这里在handleTransformer()方法中创建了一个Student对象,并调用参数的transform()方法将Student对象转换成一个字符串。

 这段代码从安全的角度来分析是没有任何问题的,因为Student是Person的子类,使用
Transformer< Person>的匿名类实现将Student对象转换成一个字符串也是绝对安全的,并不存在类型转换的安全隐患。但是实际上,在调用handleTransformer()方法的时候却会提示语法错误,原因也很简单,Transformer< Person>并不是Transformer< Student>的子类型。

 那么这个时候逆变就可以派上用场了,它就是专门用于处理这种情况的。修改Transformer接
口中的代码,如下所示:

interface Transformer<in T> {fun transform(t: T): String
}

 这里我们在泛型T的声明前面加上了一个in关键字。这就意味着现在T只能出现在in位置上,而
不能出现在out位置上,同时也意味着Transformer在泛型T上是逆变的。没错,只要做了这样一点修改,刚才的代码就可以编译通过且正常运行了,因为此时Transformer< Person>已经成为了Transformer< Student>的子类型。

 逆变的用法大概就是这样了,如果你还想再深入思考一下的话,可以想一想为什么逆变的时候
泛型T不能出现在out位置上?为了解释这个问题,我们先假设逆变是允许让泛型T出现在out位
置上的,然后看一看可能会产生什么样的安全隐患。

 修改Transformer中的代码,如下所示:

interface Transformer<in T> {fun transform(name: String, age: Int): @UnsafeVariance T
}

 可以看到,我们将transform()方法改成了接收name和age这两个参数,并把返回值类型改成了泛型T。由于逆变是不允许泛型T出现在out位置上的,这里为了能让编译器正常编译通过,所以加上了@UnsafeVariance注解,这和List源码中使用的技巧是一样的。

 那么,这个时候可能会产生什么样的安全隐患呢?我们来看一下如下代码就知道了:

fun main() {val trans = object : Transformer<Person> {override fun transform(name: String, age: Int): Person {return Teacher(name, age)}}handleTransformer(trans)
}
fun handleTransformer(trans: Transformer<Student>) {val result = trans.transform("Tom", 19)
}

 上述代码就是一个典型的违反逆变规则而造成类型转换异常的例子。在Transformer< Person>的匿名类实现中,我们使用transform()方法中传入的name和age参数构建了一个Teacher对象,并把这个对象直接返回。由于transform()方法的返回值要求是一个Person对象,而Teacher是Person的子类,因此这种写法肯定是合法的。

 但在handleTransformer()方法当中,我们调用了Transformer< Student>的transform()方法,并传入了name和age这两个参数,期望得到的是一个Student对象的返回,然而实际上transform()方法返回的却是一个Teacher对象,因此这里必然会造成类型转换异常。

 也就是说,Kotlin在提供协变和逆变功能时,就已经把各种潜在的类型转换安全隐患全部考虑进
去了。只要我们严格按照其语法规则,让泛型在协变时只出现在out位置上,逆变时只出现在in位置上,就不会存在类型转换异常的情况。虽然@UnsafeVariance注解可以打破这一语法规则,但同时也会带来额外的风险,所以你在使用@UnsafeVariance注解时,必须很清楚自己在干什么才行。

 最后我们再来介绍一下逆变功能在Kotlin内置API中的应用,比较典型的例子就是Comparable
的使用。Comparable是一个用于比较两个对象大小的接口,其源码定义如下:

interface Comparable<in T> {operator fun compareTo(other: T): Int
}

 可以看到,Comparable在T这个泛型上就是逆变的,compareTo()方法则用于实现具体的比较逻辑。那么这里为什么要让Comparable接口是逆变的呢?想象如下场景,如果我们使用Comparable< Person>实现了让两个Person对象比较大小的逻辑,那么用这段逻辑去比较两个Student对象的大小也一定是成立的,因此让Comparable< Person>成为Comparable< Student>的子类合情合理,这也是逆变非常典型的应用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/167318.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Wordpress - Xydown独立下载页面插件

Wordpress - Xydown独立下载页面插件&#xff1b; 1.使用ftp将demo.php和download.php上传到网站根目录&#xff08;两个文件中设计网站信息的代码可根据实际情况修改为自己的信息&#xff09; 使用ftp将demo.php和download.php上传到网站根目录&#xff08;两个文件中设计…

[ Windows ] ping IP + Port 测试 ip 和 端口是否通畅

开发过程中经常会黑窗口中手动测试一下计划请求的目标ip和端口是否通畅&#xff0c;测试方式如下&#xff1a; 一、单纯测试ip是否能够 ping 通&#xff0c;这个比较熟悉了&#xff0c;运行 cmd 打开黑窗口 输入如下指令&#xff0c;能够如下提示信息&#xff0c;表示端口是通…

【AIGC核心技术剖析】用于高效 3D 内容创建生成(从单视图图像生成高质量的纹理网格)

3D 内容创建的最新进展主要利用通过分数蒸馏抽样 &#xff08;SDS&#xff09; 生成的基于优化的 3D 生成。尽管已经显示出有希望的结果&#xff0c;但这些方法通常存在每个样本优化缓慢的问题&#xff0c;限制了它们的实际应用。在本文中&#xff0c;我们提出了DreamGaussian&…

Node.js、Vue的安装与使用(Linux OS)

Vue的安装与使用&#xff08;Linux OS&#xff09; Node.js的安装Vue的安装Vue的使用 操作系统&#xff1a;Ubuntu 20.04 LTS Node.js的安装 安装Node.js Node.js官方下载地址 1.选择合适的系统架构&#xff08;可通过uname -m查看&#xff09;版本安装 2.下载文件为tar.xz格…

微信小程序之个人中心授权登录

&#x1f3ac; 艳艳耶✌️&#xff1a;个人主页 &#x1f525; 个人专栏 &#xff1a;《Spring与Mybatis集成整合》《Vue.js使用》 ⛺️ 越努力 &#xff0c;越幸运。 1.了解微信授权登录 微信登录官网&#xff1a; 小程序登录https://developers.weixin.qq.com/miniprogram/d…

SystemVerilog Assertions应用指南 Chapter 1.17使用参数的SVA检验器

1.17使用参数的SVA检验器 SVA允许像 Verilog那样在检验器中使用参数( parameter)&#xff0c;这为创建可重用的属性提供了很大的灵活性。比如,两个信号间的延迟信息可以在检验器中用参数表示,那么这种检验器就可以在设计只有时序关系不同的情况中重用。例子1.2显示了个带延迟默…

人大金仓与哪吒科技达成战略合作,加快推动智慧港口建设

近日&#xff0c;人大金仓与哪吒港航智慧科技&#xff08;上海&#xff09;有限公司&#xff08;以下简称“哪吒科技”&#xff09;达成战略合作。双方旨在共享优势资源&#xff0c;联合为港口企业转型升级提供完备的技术支撑与行业解决方案。人大金仓总裁杜胜、哪吒科技总经理…

基于FPGA的图像拉普拉斯变换实现,包括tb测试文件和MATLAB辅助验证

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a vivado2019.2 3.部分核心程序 timescale 1ns / 1ps // // Company: // Engineer: // // Create Date: 202…

cdm解决‘ping‘ 或者nslookup不是内部或外部命令,也不是可运行的程序或批处理文件的问题

当我们在执行cmd时&#xff0c;会出现不是内部或外部命令&#xff0c;也不是可运行的程序的提示。 搜索环境变量 点开高级 >> 环境变量 打开Path&#xff0c;看是否在Path变量值中存在以下项目&#xff1a; %SystemRoot%/system32; %SystemRoot%; %SystemRoot%/Syste…

Opencv之RANSAC算法用于直线拟合及特征点集匹配详解

Opencv之RANSAC算法用于直线拟合及特征点集匹配详解 讲述Ransac拟合与最小二乘在曲线拟合上的优缺点 讲述在进行特征点匹配时&#xff0c;最近邻匹配与Ransac匹配的不同之处 另外&#xff0c;Ransac也被用于椭圆拟合、变换矩阵求解等 1. 直线拟合 1.1 原理 RANSAC(RANdom …

Mysql中的RR 隔离级别,到底有没有解决幻读问题

Mysql 中的 RR 事务隔离级别&#xff0c;在特定的情况下会出现幻读的问题。所谓的幻读&#xff0c;表示在同一个事务中的两次相同条件的查询得到的数据条数不一样。 在 RR 级别下&#xff0c;什么情况下会出现幻读 这样一种情况&#xff0c;在事务 1 里面通过 update 语句触发当…

【CGSSA-BP预测】基于混合混沌-高斯变异-麻雀算法优化BP神经网络回归预测研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

手机拍照转机器人末端坐标(九点标定法)

1.打印标定纸&#xff0c;随机九个点 2.让UR机器人末端分别走到P1-P9九个点 在图示位置读取九个点的X&#xff0c;Y坐标 3.手机拍照&#xff08;固定点&#xff09; 测试可以随机拍一张&#xff0c;实用的话需要固定手机的拍照位置&#xff0c;得到的图片如下&#xff1a; 4.…

使用AI编写测试用例——详细教程

随着今年chatGPT的大热&#xff0c;每个行业都试图从这项新技术当中获得一些收益我之前也写过一篇测试领域在AI技术中的探索&#xff1a;软件测试中的AI——运用AI编写测试用例现阶段AI还不能完全替代人工测试用例编写&#xff0c;但是如果把AI当做一个提高效率的工具&#xff…

基于单片机设计的家用自来水水质监测装置

一、前言 本文介绍基于单片机设计的家用自来水水质监测装置。利用STM32F103ZET6作为主控芯片&#xff0c;结合水质传感器和ADC模块&#xff0c;实现对自来水水质的检测和监测功能。通过0.96寸OLED显示屏&#xff0c;将采集到的水质数据以直观的方式展示给用户。 随着人们对健…

图论01-【无权无向】-图的基本表示-邻接矩阵/邻接表

文章目录 1. 代码仓库2. 图的基本表示的比较3. 邻接矩阵&#xff1a;Array和TreeSet3.1 图示3.2 Array主要代码解析3.3 测试输出3.4 使用TreeSet的代码 4. 邻接表&#xff1a;LinkedList4.1 图示4.2 LinkedList主要代码解析4.3 测试输出 5. 完整代码5.1 邻接表 - Array5.2 邻接…

深入理解C++红黑树的底层实现及应用

文章目录 1、红黑树简介1.1 、概述&#xff1a;介绍红黑树的定义、特点和用途。 2、红黑树节点的定义3、红黑树结构3.1、红黑树的插入操作 4、红黑树的验证4.1、红黑树的删除4.2、红黑树与AVL树的比较4.3、红黑树的应用 5、总结 1、红黑树简介 1.1 、概述&#xff1a;介绍红黑…

Linux | 深入浅出冯诺依曼

前言 但凡是科班出生的小伙伴多多稍稍应该都听过冯诺依曼体系吧&#xff0c;这似乎已成为入门计算机的必备知识了&#xff0c;本章就带着大家一起去理解冯诺依曼体系&#xff1b; 一、体系构成 冯诺依曼体系主张计算机由五大部件组成&#xff0c;如下所示&#xff1b; 输入设备…

Java设计模式 | 基于订单批量支付场景,对策略模式和简单工厂模式进行简单实现

基于订单批量支付场景&#xff0c;对策略模式和简单工厂模式进行简单实现 文章目录 策略模式介绍实现抽象策略具体策略1.AliPayStrategy2.WeChatPayStrategy 环境 使用简单工厂来获取具体策略对象支付方式枚举策略工厂接口策略工厂实现 测试使用订单实体类对订单进行批量支付结…

Stable Diffusion WebUI报错RuntimeError: Torch is not able to use GPU解决办法

新手在安装玩Stable Diffusion WebUI之后会遇到各种问题&#xff0c; 接下来会慢慢和你讲解如何解决这些问题。 在我们打开Stable Diffusion WebUI时会报错如下&#xff1a; RuntimeError: Torch is not able to use GPU&#xff1b;add --skip-torch-cuda-test to COMMANDL…