当前位置: 首页 > article >正文

Kotlin 2.1.0 入门教程(二十)扩展

扩展

Kotlin 提供了一种能力,无需继承类或使用像装饰器这样的设计模式,就能为类或接口扩展新的功能。这是通过一种名为扩展的特殊声明来实现的。

例如,你可以为无法修改的第三方库中的类或接口编写新的函数。这些函数可以像原类的方法一样以常规方式调用。这种机制被称为扩展函数。此外,还有扩展属性,它允许你为现有类定义新的属性。

扩展函数

要声明一个扩展函数,需要在函数名前加上接收者类型,该接收者类型指的是要被扩展的类型。以下代码为 MutableList<Int> 添加了一个 swap 函数:

fun MutableList<Int>.swap(index1: Int, index2: Int) {val tmp = this[index1]this[index1] = this[index2]this[index2] = tmp
}

扩展函数内部的 this 关键字对应接收者对象(即那个在点号之前传递的对象)。现在,你可以在任何 MutableList<Int> 上调用这样的函数:

val list = mutableListOf(1, 2, 3)
list.swap(0, 2)

这个函数对于任何 MutableList<T> 都有意义,可以将它泛型化:

fun <T> MutableList<T>.swap(index1: Int, index2: Int) {val tmp = this[index1]this[index1] = this[index2]this[index2] = tmp
}

你需要在函数名之前声明泛型类型参数,以便它能在接收者类型表达式中使用。有关泛型的更多信息,请参阅泛型函数。

扩展是静态解析的

扩展实际上并不会修改它们所扩展的类。通过定义一个扩展,你并没有向类中插入新的成员,只是让新的函数可以通过点号表示法在该类型的变量上调用。

扩展函数是静态分发的。因此,调用哪个扩展函数在编译时就已经根据接收者类型确定了。例如:

open class Shape
class Rectangle : Shape()fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"fun printClassName(s: Shape) {println(s.getName())
}fun main() {printClassName(Rectangle()) // Shape
}

这个示例会打印出 Shape,因为所调用的扩展函数仅取决于参数 s 的声明类型,该声明类型为 Shape 类。

如果一个类有一个成员函数,同时又定义了一个扩展函数,且该扩展函数的接收者类型、名称都与成员函数相同,并且能应用于给定的参数,那么成员函数总是会被优先调用。例如:

class Example {fun printFunctionType() {println("Class method")}
}fun Example.printFunctionType() {println("Extension function")
}fun main() {Example().printFunctionType() // Class method
}

然而,扩展函数对同名但签名不同的成员函数进行重载是完全没问题的:

class Example {fun printFunctionType() {println("Class method")}
}fun Example.printFunctionType(i: Int) {println("Extension function")
}fun main() {Example().printFunctionType(1) // Extension function
}

可空接收者

请注意,扩展可以使用可空的接收者类型来定义。即使对象变量的值为 null,也可以在该变量上调用这些扩展函数。如果接收者为 null,那么 this 也为 null。因此,在定义具有可空接收者类型的扩展时,我们建议在函数体内部进行 this == null 检查,以避免编译错误。

Kotlin 中,你可以直接调用 toString() 方法而无需检查是否为 null,因为该检查已经在扩展函数内部完成了:

fun Any?.toString(): String {if (this == null) return "null"return toString()
}

扩展属性

Kotlin 对扩展属性的支持与对扩展函数的支持非常相似:

val <T> List<T>.lastIndex: Intget() = size - 1

由于扩展实际上并不会向类中插入成员,因此扩展属性无法高效地拥有幕后字段。这就是为什么扩展属性不允许使用初始化器的原因。扩展属性的行为只能通过显式提供 getter / setter 来定义。

示例:

// 错误:扩展属性不允许使用初始化器。
val House.number = 1

伴生对象扩展

如果一个类定义了伴生对象,你也可以为伴生对象定义扩展函数和扩展属性。

就像伴生对象的常规成员一样,调用它们时只需使用类名作为限定符:

class MyClass {// 该伴生对象将被称为 Companion。companion object { }
}fun MyClass.Companion.printCompanion() {println("companion")
}fun main() {MyClass.printCompanion()
}

扩展的作用域

在大多数情况下,你会在顶层直接在包下定义扩展:

package org.example.declarationsfun List<String>.getLongestString() { /*...*/ }

要在声明扩展的包之外使用该扩展,需在调用处导入它:

package org.example.usageimport org.example.declarations.getLongestStringfun main() {val list = listOf("red", "green", "blue")list.getLongestString()
}

将扩展声明为成员

你可以在一个类内部为另一个类声明扩展。在这样的扩展内部,存在多个隐式接收者,这些对象的成员可以无需限定符即可访问。声明扩展的类的实例被称为分发接收者,而扩展方法的接收者类型的实例被称为扩展接收者。

class Host(val hostname: String) {fun printHostname() { print(hostname) }
}class Connection(val host: Host, val port: Int) {fun printPort() { print(port) }fun Host.printConnectionString() {// 调用 Host.printHostname()。printHostname()print(":")// 调用 Connection.printPort()。printPort()}fun connect() {// 调用扩展函数。host.printConnectionString()}
}fun main() {Connection(Host("kotl.in"), 443).connect()// 错误,该扩展函数在 Connection 外部不可用。// Host("kotlin").printConnectionString()
}

如果分发接收者和扩展接收者的成员发生名称冲突,扩展接收者的成员优先。若要引用分发接收者的成员,你可以使用限定 this 语法。

class Connection {fun Host.getConnectionString() {// 调用 Host.toString()。toString()// 调用 Connection.toString()。this@Connection.toString()}
}

作为成员声明的扩展可以被声明为 open 并在子类中重写。这意味着此类函数的分发对于分发接收者类型是虚拟的,但对于扩展接收者类型是静态的。

open class Baseclass Derived : Base()open class BaseCaller {open fun Base.printFunctionInfo() {println("Base extension function in BaseCaller")}open fun Derived.printFunctionInfo() {println("Derived extension function in BaseCaller")}fun call(b: Base) {b.printFunctionInfo()}
}class DerivedCaller : BaseCaller() {override fun Base.printFunctionInfo() {println("Base extension function in DerivedCaller")}override fun Derived.printFunctionInfo() {println("Derived extension function in DerivedCaller")}
}fun main() {BaseCaller().call(Base()) // Base extension function in BaseCallerBaseCaller().call(Derived()) // Base extension function in BaseCallerDerivedCaller().call(Base()) // Base extension function in DerivedCallerDerivedCaller().call(Derived()) // Base extension function in DerivedCaller
}

输出结果及原因分析

BaseCaller().call(Base()):输出 Base extension function in BaseCaller

  • BaseCaller 类的 call 方法中,参数 b 的类型是 Base

  • 当调用 b.printFunctionInfo() 时,由于 bBase 类型,会调用 BaseCaller 类中为 Base 类定义的扩展函数 Base.printFunctionInfo(),所以输出 Base extension function in BaseCaller

BaseCaller().call(Derived()):输出 Base extension function in BaseCaller

  • 虽然传递给 call 方法的实际对象是 Derived 类型,但 call 方法的参数类型声明为 Base

  • 扩展函数是静态解析的,也就是说,调用哪个扩展函数是根据参数的声明类型来决定的,而不是实际类型。因此,这里仍然会调用 BaseCaller 类中为 Base 类定义的扩展函数 Base.printFunctionInfo(),输出 Base extension function in BaseCaller

DerivedCaller().call(Base()):输出 Base extension function in DerivedCaller

  • DerivedCaller 继承自 BaseCaller,并且重写了 Base 类的扩展函数 Base.printFunctionInfo()

  • 当调用 DerivedCaller().call(Base()) 时,call 方法在 BaseCaller 类中定义,但是在 DerivedCaller 实例上调用,由于 DerivedCaller 重写了 Base 类的扩展函数,所以会调用重写后的扩展函数,输出 Base extension function in DerivedCaller

DerivedCaller().call(Derived()):输出 Base extension function in DerivedCaller

  • 同样,call 方法的参数类型声明为 Base,扩展函数是静态解析的,根据参数的声明类型来决定调用哪个扩展函数。

  • 因为是在 DerivedCaller 实例上调用 call 方法,而 DerivedCaller 重写了 Base 类的扩展函数,所以会调用重写后的 Base 类的扩展函数,输出 Base extension function in DerivedCaller

可见性说明

扩展使用的可见性修饰符,与在相同作用域中声明的普通函数所用的可见性修饰符相同。例如:

  • 在文件顶层声明的扩展,可以访问同一文件中的其他 private 顶层声明。

  • 如果在接收者类型之外声明扩展,它无法访问接收者的 privateprotected 成员。

相关文章:

Kotlin 2.1.0 入门教程(二十)扩展

扩展 Kotlin 提供了一种能力&#xff0c;无需继承类或使用像装饰器这样的设计模式&#xff0c;就能为类或接口扩展新的功能。这是通过一种名为扩展的特殊声明来实现的。 例如&#xff0c;你可以为无法修改的第三方库中的类或接口编写新的函数。这些函数可以像原类的方法一样以…...

神经网络的学习 求梯度

import sys, ossys.path.append(os.pardir) import numpy as npfrom common.functions import softmax, cross_entropy_error from common.gradient import numerical_gradient# simpleNet类 class simpleNet:def __init__(self):self.W np.random.rand(2, 3) # 随机形状为2*…...

机器学习数学基础:24.随机事件与概率

一、教程目标 本教程致力于帮助零基础或基础薄弱的学习者&#xff0c;全面掌握概率论与数理统计的基础公式&#xff0c;透彻理解核心概念&#xff0c;熟练学会应用解题技巧&#xff0c;最终能够轻松应对期末或考研考试。 二、适用人群 特别适合那些对概率论与数理统计知识了…...

【NLP 24、模型训练方式】

你的痛苦&#xff0c;我都心疼&#xff0c;想为你解决 —— 25.2.15 一、按学习范式分类 1. 监督学习&#xff08;Supervised Learning&#xff09; 核心思想&#xff1a;使用带有标签&#xff08;已知输入-输出对&#xff09;的数据训练模型。 常见任务&#xff1a;分类&…...

【鸿蒙】ArkUI-X跨平台问题集锦

系列文章目录 【鸿蒙】ArkUI-X跨平台问题集锦 文章目录 系列文章目录问题集锦1、HSP,HAR模块中 无法引入import bridge from arkui-x.bridge;2、CustomDialog 自定义弹窗中的点击事件在Android 中无任何响应&#xff1b;3、调用 buildRouterMode() 路由跳转页面前&#xff0c;…...

AI向量数据库之LanceDB快速介绍

LanceDB LanceDB 是一个开源的向量搜索数据库&#xff0c;具备持久化存储功能&#xff0c;极大地简化了嵌入向量的检索、过滤和管理。 LanceDB的主要特点 LanceDB 的主要特点包括&#xff1a; 生产级向量搜索&#xff1a;无需管理服务器。 存储、查询和过滤向量、元数据以…...

嵌入式玩具--无人机字幕

day01 01-无人机-组成结构-上 哎&#xff0c;好&#xff0c;各位&#xff0c;那现在呢我们一起来看一下&#xff0c;就是咱们接下来要做的这个小项目啊。呃&#xff0c;当然这个名字有很多啊&#xff0c;就是这种飞行器有管&#xff0c;它叫四旋翼飞行器的&#xff0c;也有叫…...

CentOS7 安装配置FTP服务

CentOS7 安装配置FTP服务 CentOS7 安装配置FTP服务1. FTP简介2. 先行准备2.1 关闭防火墙2.2 关闭 SELinux 3.安装FTP软件包4. 创建 FTP 用户及目录4.1 创建 FTP 目录并设置权限4.2 防止 FTP 用户登录 Linux 终端4.3 创建 FTP 用户组及用户4.4 创建 FTP 可写目录 5. 配置ftp服务…...

几款dxf文件转Gcode的开源软件

以下是一些常用的开源软件,可以将DXF文件转换为Gcode: 1. **Inkscape with Gcode Tools** - **Inkscape** 是一款开源的矢量图形编辑器,支持DXF文件导入。通过安装 **Gcode Tools** 插件,可以将矢量图形转换为Gcode。 - 官网: [Inkscape](https://inkscape.org/) …...

【设计模式】03-理解常见设计模式-行为型模式(专栏完结)

前言 前面我们介绍完创建型模式和创建型模式&#xff0c;这篇介绍最后的行为型模式&#xff0c;也是【设计模式】专栏的最后一篇。 一、概述 行为型模式主要用于处理对象之间的交互和职责分配&#xff0c;以实现更灵活的行为和更好的协作。 二、常见的行为型模式 1、观察者模…...

【计算机网络】传输层数据段格式

在计算机网络中&#xff0c;数据段&#xff08;Segment&#xff09; 是传输层协议&#xff08;如 TCP 或 UDP&#xff09;使用的数据单元。TCP 和 UDP 的数据段格式有所不同&#xff0c;以下是它们的详细说明&#xff1a; 1. TCP 数据段格式 TCP&#xff08;传输控制协议&…...

编程题-最大子数组和(中等-重点【贪心、动态规划、分治思想的应用】)

题目&#xff1a; 给你一个整数数组 nums &#xff0c;请你找出一个具有最大和的连续子数组&#xff08;子数组最少包含一个元素&#xff09;&#xff0c;返回其最大和。 子数组是数组中的一个连续部分。 解法一&#xff08;枚举法-时间复杂度超限&#xff09;&#xff1a; …...

网络将内网服务转换到公网上

当然&#xff0c;以下是根据您提供的描述&#xff0c;对内网端口在公网上转换过程的详细步骤&#xff0c;并附上具体例子进行说明&#xff1a; 内网端口在公网上的转换过程详细步骤 1. 内网服务配置 步骤说明&#xff1a; 在内网中的某台计算机&#xff08;我们称之为“内网…...

本地通过隧道连接服务器的mysql

前言 服务器上部署了 mysql&#xff0c;本地希望能访问该 mysql&#xff0c;但是又不希望 mysql 直接暴露在公网上 那么可以通过隧道连接 ssh 端口的方式进行连接 从外网看&#xff0c;服务器只开放了一个 ssh 端口&#xff0c;并没有开放 3306 监听端口 设置本地免密登录 …...

跳跃游戏 II - 贪心算法解法

问题描述&#xff1a; 给定一个长度为 n 的 0 索引整数数组 nums&#xff0c;我们从数组的第一个元素 nums[0] 开始。每个元素 nums[i] 表示从索引 i 可以跳跃的最大长度&#xff0c;换句话说&#xff0c;从位置 i&#xff0c;你可以跳到位置 i j&#xff0c;其中 0 < j &…...

2. grafana插件安装并接入zabbix

一、在线安装 如果不指定安装位置&#xff0c;则默认安装位置为/var/lib/grafana/plugins 插件安装完成之后需要重启grafana 命令在上一篇讲到过 //查看相关帮助 [rootlocalhost ~]# grafana-cli plugins --help //从列举中的插件过滤zabbix插件 [rootlocalhost ~]# grafana…...

Linux第107步_Linux之PCF8563实验

使用PCF8563代替内核的RTC&#xff0c;可以降低功耗&#xff0c;提高时间的精度。同时有助于进一步熟悉I2C驱动的编写。 1、了解rtc_time64_to_tm()和rtc_tm_to_time64() 打开“drivers/rtc/lib.c” /* * rtc_time64_to_tm - Converts time64_t to rtc_time. * Convert seco…...

功能说明并准备静态结构

功能说明并准备静态结构 <template><div class"card-container"><!-- 搜索区域 --><div class"search-container"><span class"search-label">车牌号码&#xff1a;</span><el-input clearable placeho…...

pip 与 conda 的故事

pip 换源 pip 官方源 -i https://pypi.python.org/simple pip 清华源 -i https://pypi.tuna.tsinghua.edu.cn/simple pip 阿里源 -i https://mirrors.aliyun.com/pypi/simple PyTorch 安装 pip3 install torch torchvision torchaudio pip3 install torch torchvision torchaud…...

【05】RUST错误处理

文章目录 错误处理panic代码运行 ResutResult中的一些方法介绍传播错误&#xff1f;运算符 错误处理 建议是尽量用Result由调用者自行决定是否恢复&#xff0c;不恢复也可直接在Err中调用panic。代码分支不可能走的分支可panic。 需要panic的情况&#xff1a; 有害状态&#x…...

[免费]SpringBoot公益众筹爱心捐赠系统【论文+源码+SQL脚本】

大家好&#xff0c;我是老师&#xff0c;看到一个不错的SpringBoot公益众筹爱心捐赠系统&#xff0c;分享下哈。 项目介绍 公益捐助平台的发展背景可以追溯到几十年前&#xff0c;当时人们已经开始通过各种渠道进行公益捐助。随着互联网的普及&#xff0c;本文旨在探讨公益事业…...

算法【动态规划中使用观察优化枚举】

动态规划的问题中&#xff0c;已经写出了记忆化搜索的版本&#xff0c;还要写出严格位置依赖的版本&#xff0c;意义在于不仅可以进行空间压缩优化&#xff1b;关键还在于&#xff0c;很多时候通过进一步观察&#xff0c;可以优化枚举&#xff0c;让时间复杂度更好。优化枚举的…...

ML.Net二元分类

ML.Net二元分类 文章目录 ML.Net二元分类前言项目的创建机器学习模型的创建添加模型选择方案训练环境的选择训练数据的添加训练数据的选择训练数据的格式要预测列的选择模型评估模型的使用总结前言 ‌ML.NET‌是由Microsoft为.NET开发者平台创建的免费、开源、跨平台的机器学习…...

visutal studio 2022使用qcustomplot基础教程

编译 下载&#xff0c;2.1.1版支持到Qt6.4 。 拷贝qcustomplot.h和qcustomplot.cpp到项目源目录&#xff08;Qt project&#xff09;。 在msvc中将它俩加入项目中。 使用Qt6.8&#xff0c;需要修改两处代码&#xff1a; L6779 # if QT_VERSION > QT_VERSION_CHECK(5, 2, …...

本地搭建自己的专属客服之OneApi关联Ollama部署的大模型并创建令牌《下》

这里写目录标题 OneApi1、渠道设置2、令牌创建 配置文件修改修改配置文件docker-compose.yml修改config.json到此结束 上文讲了如何本地docker部署fastGtp&#xff0c;相信大家也都已经部署成功了&#xff01;&#xff01;&#xff01; 今天就说说怎么让他们连接在一起 创建你的…...

c#自动更新-源码

软件维护与升级 修复漏洞和缺陷&#xff1a;软件在使用过程中可能会发现各种漏洞和缺陷&#xff0c;自动更新可以及时推送修复程序&#xff0c;增强软件的稳定性和安全性&#xff0c;避免因漏洞被利用而导致数据泄露、系统崩溃等问题。提升性能&#xff1a;通过自动更新&#x…...

SIP中常见的服务器类型

在SIP&#xff08;Session Initiation Protocol&#xff09;网络中&#xff0c;除了B2BUA&#xff08;Back-to-Back User Agent&#xff09;、路由代理和媒体服务器外&#xff0c;还有其他类型的服务器。以下是所有类型的服务器及其作用、示例和其他相关信息的表格&#xff1a;…...

【C】初阶数据结构4 -- 双向循环链表

之前学习的单链表相比于顺序表来说&#xff0c;就是其头插和头删的时间复杂度很低&#xff0c;仅为O(1) 且无需扩容&#xff1b;但是对于尾插和尾删来说&#xff0c;由于其需要从首节点开始遍历找到尾节点&#xff0c;所以其复杂度为O(n)。那么有没有一种结构是能使得头插和头删…...

小爱音箱控制手机和电视听歌的尝试

最近买了小爱音箱pro&#xff0c;老婆让我扔了&#xff0c;吃灰多年的旧音箱。当然舍不得&#xff0c;比小爱还贵&#xff0c;刚好还有一台红米手机&#xff0c;能插音箱&#xff0c;为了让音箱更加灵活&#xff0c;买了个2元的蓝牙接收模块Type-c供电3.5接口。这就是本次尝试起…...

Kotlin Lambda

Kotlin Lambda 在探索Kotlin Lambda之前&#xff0c;我们先回顾下Java中的Lambda表达式&#xff0c;Java 的 Lambda 表达式是 Java 8 引入的一项强大的功能&#xff0c;它使得函数式编程风格的代码更加简洁和易于理解。Lambda 表达式允许你以一种更简洁的方式表示实现接口&…...