Compose 实践与探索十二 —— 附带效应
1、SideEffect
Google 官方文档对 side effect 有两种翻译,简体中文翻译为附带效应,繁体中文翻译为副作用。这两个翻译我们用哪个都行,关键是如何理解它的含义。
1.1 什么是副作用
我们在日常生活中听到的副作用大多是医学领域中的,药物的副作用是指除了预期治疗效果外,在使用过程中可能产生的额外不良反应或不良影响。可以理解为,副作用是目标作用为了生效而附带而来的作用,它并不是“负作用”那种完全不好的、负面的作用。
在 Compose 中,副作用(或附带效应,后续出现两个词都认为是 side effect)通常指的是对界面以外的状态进行更改或者操作的行为。在函数式编程中,强调函数的纯粹性,即函数的输出仅依赖于输入,不会对外部状态产生影响。然而,在 UI 开发中,通常需要与外部环境进行交互,比如修改变量、进行网络请求等,这些会导致“副作用”。
比如说:
fun a() {var value = 0value = 1
}var flag = false
fun b() {flag = true
}
函数 a 只修改了其内部的变量,因此没有副作用;而函数 b 修改了外部的变量 flag,因此它有副作用。
再看:
fun c() {println("Compose")
}
想要确认函数 c 是否有副作用,通过判断是否修改函数内部变量的方式似乎不能确定,这里我们可以借助副作用的学术性定义:对于一个函数,如果用它的返回值替换函数本身,但不会对程序有任何影响,那么该函数就没有副作用,假如产生了影响,两种结果之间的差异就是副作用。
对于 println() 这个函数而言,它本身没有返回值,或者说是一个 Unit,用返回值替换函数本身使得无法打印指定内容,这对程序产生了影响,因此 println 这个函数是有副作用的,函数 c 也因此是有副作用的。
副作用这个词可以很好地描述函数的纯净性,可以称一个函数是“有/无副作用的函数”。Compose 就要求所有的组件函数都是无副作用的函数。Compose 的 @Composable 函数是用来显示界面内容的,应该只包含界面显示工作,不应掺杂其他任何对外界有影响的工作,也就是不应该有副作用。因为 @Composable 函数的副作用会导致整个程序产生不可预期的结果,这是由于 @Composable 函数的调用就具有不可预期性。
由于 Compose 框架对于重组过程的优化,一个 @Composable 函数可能在运行过程中被终断甚至干脆就没被执行,这样可能会出现影响外界的代码,有一部分被执行了,剩余的部分由于终断而没被执行,从而产生不可预期的结果。
此外,由于重组的次数不确定,具有副作用的 @Composable 函数的执行结果也是不可预期的:
setContent {var seasonCount = 0Column {val seasons = arrayOf("Spring", "Summer", "Autumn", "Winter")seasons.forEach {Text(it)// 在重组之后确定 Column 仍会显示才执行代码块的内容,但此时// 最下面的 Text 要显示的内容已经确定了,无法再更改SideEffect {seasonCount++ }}Text("Total season count: $seasonCount")}}
按照设想,最后 Text 展示的 seasonCount 应该为 4,但假如代码运行过程中出现多次重组,那么 seasonCount 就不会是 4 了,结果不在预期中。
正是因为种种的不可预期性,Compose 建议开发者不要在 @Composable 函数中引入副作用代码。但很多时候,业务需求使得我们无法遵守这个建议,比如在代码中埋点统计函数的执行数据,那难道因为 Compose 的建议就无法完成这些业务需求吗?当然不是,任何时候,业务都是优先的,没有商业和业务支撑的代码创造不出任何价值,也就一文不值。
1.2 SideEffect 函数
Compose 提供了一些函数来满足业务需求(比如埋点),最直接与最简单的就是 SideEffect()。
SideEffect() 内的代码不会在执行到它时立即被执行,而是先被保存起来进行等待,直到本轮重组过程完成,确定了 SideEffect 所在的组件会在界面上显示,才会执行其内部代码。这样可以保证没有执行完就被取消的 Composable 函数的副作用代码不会被执行,还可保证在一轮重组过程中被多次调用的 Composable 函数的代码只被执行一次。
那使用 SideEffect 是不是能解决所有的副作用相关的问题呢?当然不是,想通过 SideEffect 解决副作用问题有一个前提,就是引入副作用代码的这个需求必须是正常的,不能对外界造成不可预期影响的需求。比如还是上面的例子,由于对 seasonCount 的自增操作会对外界造成不可预期的影响,因此只是简单的为它包上一层 SideEffect 并不能达到预期的结果:
setContent {var seasonCount = 0Column {val seasons = arrayOf("Spring", "Summer", "Autumn", "Winter")seasons.forEach {Text(it)// 在重组之后确定 Column 仍会显示才执行代码块的内容,但此时最下面的 Text// 要显示的内容已经确定了,无法再更改,再运行 seasonCount++ 已经晚了SideEffect {seasonCount++ }}Text("Total season count: $seasonCount")}}
真正的解决办法是你要把业务(可以简单的理解为数据处理)与界面显示分拆,在显示 UI 前把数据准备好,而不是滥用 SideEffect:
setContent {var seasonCount = 0Column {val seasons = arrayOf("Spring", "Summer", "Autumn", "Winter")seasonCount = seasons.size()seasons.forEach {Text(it)/*SideEffect {seasonCount++ }*/}Text("Total season count: $seasonCount")}}
2、DisposableEffect
DisposableEffect 是 SideEffect 的升级版,增加了对离开界面的监听。比如:
Button(onClick = { /*TODO*/ }) {DisposableEffect(Unit) {// Button 进入页面监听println("Button 进入页面")// 该 lambda 表达式必须返回一个 DisposableEffectResult,可以通过// 主动调用组件离开页面的监听函数 onDispose() 得到onDispose { println("Button 离开页面") }}}
通过主动调用 onDispose() 设置对组件离开页面的监听内容,这样在所属组件离开页面(准确点说是离开组合 Composition)时就会回调 onDispose(),同理进入页面会回调 DisposableEffect() 的内容。
有两种场景适用 DisposableEffect:
- 埋点,统计用户进入以及退出了哪些界面
- 组件进入页面时在 DisposableEffect() 内为该组件设置监听器,组件离开页面时在 onDispose() 内取消监听器
此外,我们要看一下 DisposableEffect 的第一个参数 key1:
@Composable
@NonRestartableComposable
fun DisposableEffect(key1: Any?,effect: DisposableEffectScope.() -> DisposableEffectResult
) {remember(key1) { DisposableEffectImpl(effect) }
}
它的作用是,当传入 DisposableEffect() 的 key1 发生变化时,整个 DisposableEffect() 会进行一次重启,重启动作包括两步:
- 先对老的 key1 值执行一次离开回调 onDispose()
- 然后对新的 key1 值执行一次 effect 参数函数中的进入回调
这样的顺序可以保证程序有一个合理的执行过程。比如 onDispose() 中会执行将对象 A 置为 null 的操作,而 DisposableEffect() 会为 A 赋值。那么当 DisposableEffect 因为 key1 的变化而重启时,就会先将 A 置为 null 然后再为它赋新值,而不是先为 A 赋了新值再置为 null。
反之,如果 key1 不变,不论 DisposableEffect 所在的组件如何进行重组,DisposableEffect 都不会重启(避免资源消耗):
@Composable
fun DisposableEffectSample1() {var showText by remember { mutableStateOf(false) }Button(onClick = { showText = !showText }) {Text("点击")if (showText) {Text("Compose")}// 只要 Button 重组就会回调SideEffect {println("SideEffect")}// key1 不变不论 Button 如何重组,DisposableEffect 都不会重启DisposableEffect(Unit) {println("Button 进入页面")onDispose { println("Button 离开页面") }}}
}
不断点击 Button 让其发生重组,但是只有 SideEffect() 会跟随重组进行重启,由于 DisposableEffect() 的 key1 参数不变,所以只有首次进入页面时的 log 被输出:
Button 进入页面
SideEffect
SideEffect
SideEffect
但将 DisposableEffect() 的 key1 参数换为 showText 之后,点击按钮会触发 DisposableEffect() 重启:
Button 进入页面
SideEffect
Button 离开页面
Button 进入页面
SideEffect
Button 离开页面
Button 进入页面
SideEffect
并且你能看到,DisposableEffect 会先触发 onDispose() 回调,再回调自己内部的代码。
3、LaunchedEffect
LaunchedEffect 会在 Composable 组件完成显示之后启动协程,并在参数发生改变之后重启协程。
LaunchedEffect 从功能与底层实现上来讲是特殊形式的 DisposableEffect:
@Composable
@NonRestartableComposable
fun DisposableEffect(key1: Any?,effect: DisposableEffectScope.() -> DisposableEffectResult
) {remember(key1) { DisposableEffectImpl(effect) }
}
key1 变化才会执行 DisposableEffectImpl():
private class DisposableEffectImpl(private val effect: DisposableEffectScope.() -> DisposableEffectResult
) : RememberObserver {private var onDispose: DisposableEffectResult? = nulloverride fun onRemembered() {onDispose = InternalDisposableEffectScope.effect()}override fun onForgotten() {onDispose?.dispose()onDispose = null}override fun onAbandoned() {// Nothing to do as [onRemembered] was not called.}
}
而 LaunchedEffect 也是在 key1 变化时才执行 LaunchedEffectImpl():
@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(key1: Any?,block: suspend CoroutineScope.() -> Unit
) {val applyContext = currentComposer.applyCoroutineContextremember(key1) { LaunchedEffectImpl(applyContext, block) }
}
LaunchedEffectImpl 也实现了 RememberObserver,并且实现内容都是基于协程的:
internal class LaunchedEffectImpl(parentCoroutineContext: CoroutineContext,private val task: suspend CoroutineScope.() -> Unit
) : RememberObserver {private val scope = CoroutineScope(parentCoroutineContext)private var job: Job? = nulloverride fun onRemembered() {job?.cancel("Old job was still running!")job = scope.launch(block = task)}override fun onForgotten() {job?.cancel()job = null}override fun onAbandoned() {job?.cancel()job = null}
}
何时会用到?实际上就是把组件显示到界面作为某种业务的触发逻辑时,实际上 DisposableEffect 也是这种逻辑,只不过 LaunchedEffect 是面向协程的。比如,某个组件在页面中显示 3 秒钟后消失(如刚进入视频播放页面后,视频播放的控制面板会显示几秒后消失)。
4、rememberUpdatedState
依赖但又不希望重启协程
上两节我们分别讲了 DisposableEffect 与 LaunchedEffect,它俩有一个共同的功能,就是根据参数上传入的 key 是否发生变化决定是否重启自身的执行。即参数 key 变了就重启,不变的话即便所在组件重组也不会重启,避免资源消耗。
以往我们遇到的大多数情况,都是组件依赖的状态发生变化会在同一帧中立即起到作用,比如以下这种极度简化过的代码:
var text by remember { mutableStateOf("Compose") }
...
Text(text)
但有一些场景下,我们希望被依赖的状态发生改变时,不去触发重组或 DisposableEffect 与 LaunchedEffect 的重启,也能在需要使用这个状态时拿到它最新的值:
@Composable
fun RememberUpdatedStateSample() {var welcome by remember { mutableStateOf("Initial value.") }Button(onClick = { welcome = "Jetpack Compose" }) {Text("点击")LaunchedEffect(Unit) {delay(3000)println("welcome: $welcome")}}
}
如果在 LaunchedEffect 的 3 秒延时之内点击按钮,那么 welcome 在打印时会输出更新后的 “Jetpack Compose”,而不是初始值。这种更新不需要将 welcome 作为 LaunchedEffect 的参数,在可以获取到 welcome 新值的同时,还避免了 LaunchedEffect 重启带来的性能损耗。
但假如将 LaunchedEffect 抽取到一个单独的函数中,即便在 3 秒内点击按钮,welcome 也只打印初始值:
@Composable
fun RememberUpdatedStateSample() {var welcome by remember { mutableStateOf("Initial value.") }Button(onClick = { welcome = "Jetpack Compose" }) {Text("点击")CustomLaunchedEffect(welcome)}
}@Composable
private fun CustomLaunchedEffect(welcome: String) {LaunchedEffect(Unit) {delay(3000)println("welcome: $welcome")}
}
为什么第一种情况可以,第二种情况不行呢?因为第一种情况的 welcome 通过 remember() + mutableStateOf() 实现了一个持久存储且可以将状态变化通知到所有使用处的变量,因此它可以跨越重组传递到 Button 的内部,在发生变化时可以同步给 LaunchedEffect()。而第二种情况,将 welcome 作为函数参数传递,那么 CustomLaunchedEffect() 中的 welcome 就是一个普通的变量,它的变化不会同步给 LaunchedEffect() 内的 welcome,因此即便给 CustomLaunchedEffect() 的传参发生了变化,但打印输出的 welcome 仍是协程最初拿到的初始值。
那如何解决呢?把 welcome 填到 LaunchedEffect() 的参数上?我们的要求是尽量避免让 LaunchedEffect() 重启,因此这样不行。所以还是效仿第一种情况,用 remember() + mutableStateOf() 构造一个状态变量,然后把参数 welcome 传给该状态变量:
@Composable
private fun CustomLaunchedEffect(welcome: String) {var rememberedWelcome by remember { mutableStateOf(welcome) }rememberedWelcome = welcomeLaunchedEffect(Unit) {delay(3000)println("welcome: $rememberedWelcome")}
}
LaunchedEffect() 之前的两个语句可以用 rememberUpdatedState() 平替:
@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {mutableStateOf(newValue)
}.apply { value = newValue }
也就是:
@Composable
private fun CustomLaunchedEffect(welcome: String) {val rememberedWelcome by rememberUpdatedState(welcome)LaunchedEffect(Unit) {delay(3000)println("welcome: $rememberedWelcome")}
}
rememberUpdatedState() 除了可以用于 LaunchedEffect(),也可用于 DisposableEffect(),比如:
@Composable
fun CustomDisposableEffect(user: User) {DisposableEffect(Unit) {// 拿不到 user 的新值suscriber.subscribe(user)onDispose {suscriber.unsubscribe()}}
}
在 DisposableEffect() 进行订阅操作时,拿不到参数 user 的新值,因此还是要使用 rememberUpdatedState() 来解决:
@Composable
fun CustomDisposableEffect(user: User) {val updatedUser by rememberUpdatedState(user)DisposableEffect(Unit) {suscriber.subscribe(user)onDispose {suscriber.unsubscribe()}}
}
5、rememberCoroutineScope
rememberCoroutineScope() 是在 Compose 中除了 LaunchedEffect() 之外,另一种使用协程的方式。
在 Compose 中使用协程不能像通用的协程使用方法那样,比如不可以直接使用 lifecycleScope.launch(),因为 lifecycleScope 作为一个 CoroutineScope 是用来管理协程的,主要负责在与它绑定的具有生命周期的组件结束后,自动结束该组件中运行的协程。而 Composable 函数也是具有声明周期的,在 Composable 函数内启动的协程也应该在函数结束后自动结束,这意味着每个 Composable 函数都有自己的 CoroutineScope。
因此在 Composable 函数中应该使用相应的 CoroutineScope,而不是与 Activity 生命周期绑定的 lifecycleScope。使用 rememberCoroutineScope() 可以获取到与当前组合点绑定的 CoroutineScope,然后在 remember() 中可以直接用它启动协程:
val coroutineScope = rememberCoroutineScope()
// 不用 remember 包上 launch() 会报错,因为遇到重组时每次都会重新启动一次协程
val coroutine = remember { coroutineScope.launch { } }
同样是在 Compose 中启动一个协程,LaunchedEffect() 的内部实际上已经为使用者完成了 CoroutineScope 的获取与 remember() 的使用:
@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(key1: Any?,block: suspend CoroutineScope.() -> Unit
) {val applyContext = currentComposer.applyCoroutineContextremember(key1) { LaunchedEffectImpl(applyContext, block) }
}
因此,通常我们使用 LaunchedEffect() 启动协程就足够了。但如果想要在 Composable 组件的外面启动协程时,需要使用 rememberCoroutineScope():
val coroutineScope = rememberCoroutineScope()
// 点击 Box 触发 clickable 回调时才启动协程,这是在组件外部启动的协程
Box(Modifier.clickable { coroutineScope.launch { } })
6、协程或其他状态向 Compose 状态的转换
本节主要讲如何将非 Compose 状态转换为 Compose 状态。
6.1 DisposableEffect
之前说过 DisposableEffect() 可以用来做一些订阅工作,并且可以在它的 onDispose() 回调中取消订阅。这种用法也可以用在订阅数据更新上,比如说地图上要显示一个坐标点,当坐标数据发生变化时 UI 应自动更新:
val geoManager: GeoManager = GeoManager()@Composable
fun UpdatePoint() {var position by remember { mutableStateOf(Point(0, 0)) }DisposableEffect(Unit) {// PositionCallback 提供最新的坐标数据 newPosval callback = object : PositionCallback { newPos ->position = newPos}// 注册回调与取消回调注册geoManager.register(callback)onDispose {// 本组件不再显示时取消注册geoManager.unregister(callback)}}
}
PositionCallback 可以提供更新后的坐标数据,而 GeoManager 在注册回调后可以接收到坐标变化,这个变化的坐标 newPos 原本是 Compose 无法识别的普通变量,经过赋值给 position 状态后,newPos 的变化可以自动应用到界面上,这就是一种将普通数据转换为 Compose 状态的简单示例。
此外,相同的套路也可用在 LiveData 转换为 State 上:
val positionData = MutableLiveData<Point>()@Composable
fun UpdatePoint(owner: LifecycleOwner) {var position by remember { mutableStateOf(Point(0, 0)) }DisposableEffect(Unit) {val observer = Observer<Point> { newPos ->position = newPos}positionData.observe(owner, observer)onDispose {positionData.removeObserver(observer)}}
}
实际上,Compose 为 LiveData 提供了扩展函数 observeAsState() 就可以将 LiveData 转换为 State:
// 需依赖 androidx.compose.runtime:runtime-livedata 方可使用
@Composable
fun <T> LiveData<T>.observeAsState(): State<T?> = observeAsState(value)@Composable
fun <R, T : R> LiveData<T>.observeAsState(initial: R): State<R> {val lifecycleOwner = LocalLifecycleOwner.current// 用初始值创建一个 State 对象val state = remember { mutableStateOf(initial) }DisposableEffect(this, lifecycleOwner) {// 更新 state 值的 Observerval observer = Observer<T> { state.value = it }// 订阅observe(lifecycleOwner, observer)// 取消订阅onDispose { removeObserver(observer) }}return state
}
6.2 LaunchedEffect
对于用到了协程的外部状态,如 Flow,就不能用 DisposableEffect 进行转换了,而是要换成 LaunchedEffect:
val positionState: StateFlow<Point> = TODO()@Composable
fun UpdatePoint(owner: LifecycleOwner) {var position by remember { mutableStateOf(Point(0, 0)) }LaunchedEffect(Unit) {positionState.collect { newPos ->position = newPos}}
}
6.3 produceState()
produceState() 创建一个 MutableState 对象并在协程中更新它的值:
@Composable
fun <T> produceState(initialValue: T,@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {val result = remember { mutableStateOf(initialValue) }LaunchedEffect(Unit) {ProduceStateScopeImpl(result, coroutineContext).producer()}return result
}
参数 producer 内定义获取状态值的代码,它会在协程中被执行用于获取最新的状态值:
val positionState: StateFlow<Point> = TODO()@Composable
fun UpdatePoint(owner: LifecycleOwner) {// 参数传入初始值val produceState = produceState(Point(0, 0)) {positionState.collect {// Flow 传来的新数据 it 赋值给 State 的真实数据对象 valuevalue = it}}
}
相当于把 LaunchedEffect() 的写法封装到 produceState() 这个便捷函数中了。
produceState() 内还可以调用一个 awaitDispose(),它可以无限期挂起协程,主要用于转换不是协程提供的状态的情况。
最后要提一嘴,StateFlow 提供了扩展函数 collectAsState() 可以直接将一个 StateFlow 转换成 State:
@Suppress("StateFlowValueCalledInComposition")
@Composable
fun <T> StateFlow<T>.collectAsState(context: CoroutineContext = EmptyCoroutineContext
): State<T> = collectAsState(value, context)@Composable
fun <T : R, R> Flow<T>.collectAsState(initial: R,context: CoroutineContext = EmptyCoroutineContext
): State<R> = produceState(initial, this, context) {if (context == EmptyCoroutineContext) {collect { value = it }} else withContext(context) {collect { value = it }}
}
它内部就是用到了 produceState()。
7、把 Compose 的 State 转换成协程的 Flow
snapshotFlow() 可以把 Compose 的 State 转换成协程 Flow:
setContent {var name by remember { mutableStateOf("Jack") }var age by remember { mutableStateOf(18) }val flow = snapshotFlow { "$name $age" }LaunchedEffect(Unit) {// snapshotFlow() 内任何一个状态发生变化,都会以新值执行一次 collectflow.collect { info ->println(info)}}}
在Compose中,副作用通常发生在LaunchedEffect、DisposableEffect、SideEffect等函数中。这些函数用于处理可能会引起副作用的操作,如启动协程、订阅数据、修改可变状态等。需要注意的是,在Compose中,副作用应该尽量被限制在特定的作用域内,以保持代码的可维护性和可预测性。
总的来说,Compose中的“副作用”指的是对外部状态进行更改或操作的行为,通过合适的方式管理和控制副作用的产生,可以帮助确保应用的正确性和性能。
相关文章:
Compose 实践与探索十二 —— 附带效应
1、SideEffect Google 官方文档对 side effect 有两种翻译,简体中文翻译为附带效应,繁体中文翻译为副作用。这两个翻译我们用哪个都行,关键是如何理解它的含义。 1.1 什么是副作用 我们在日常生活中听到的副作用大多是医学领域中的&#x…...
Kubernetes 控制平面详解 —— 探秘 API Server、Controller Manager、Scheduler 与 etcd
文章目录 Kubernetes 控制平面详解 —— 探秘 API Server、Controller Manager、Scheduler 与 etcd控制平面概述API Server角色与职责工作原理 etcd角色与职责工作原理 Scheduler角色与职责工作原理 Controller Manager角色与职责工作原理 总结 Kubernetes 控制平面详解 —— 探…...
SSM基础专项复习4——Maven项目管理工具(1)
系列文章 1、SSM基础专项复习1——SSM项目整合-CSDN博客 2、SSM基础专项复习2——Spring 框架(1)-CSDN博客 3、SSM基础专项复习3——Spring框架(2)-CSDN博客 文章目录 系列文章 1. Maven 的概念 1.1. 什么是 Maven 1.2. 什…...
使用c#进行串口通信
一、串口通信协议 1.串口通信协议简介 串口通信(serial communication)是一种设备间非常常用的串行通信方式,大部分电子设备都支持,电子工程师再调试设备时也经常使用该通信方式输出调试信息。讲到某一种通信协议,离…...
Web开发-PHP应用鉴别修复AI算法流量检测PHP.INI通用过滤内置函数
知识点: 1、安全开发-原生PHP-PHP.INI安全 2、安全开发-原生PHP-全局文件&单函数 3、安全开发-原生PHP-流量检测&AI算法 一、演示案例-WEB开发-修复方案-PHP.INI配置 文章参考: https://www.yisu.com/ask/28100386.html https://blog.csdn.net/…...
蓝桥模拟+真题讲解
今天谁一篇文章哈 ! 由于本篇文章有些的题目只有图片,因此还望各位见谅。 目录 第一题 题目解析 代码原理 代码编写 填空技巧---巧用python 第二题 题目解析 编辑 填空技巧---巧用python 第三题 题目链接 题目解析 必备知识 解题技巧 …...
C语言【数据结构】:时间复杂度和空间复杂度.详解
引言 详细介绍什么是时间复杂度和空间复杂度。 前言:为什么要学习时间复杂度和空间复杂度 算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时…...
大模型的参数数量与学习的知识数量之间
大模型的参数数量与学习的知识数量之间 大模型的参数数量与学习的知识数量之间呈现非线性、条件依赖的复杂关系,其本质是**「表达能力」与「知识编码效率」的动态博弈**。以下从五个维度拆解核心逻辑: 一、参数是知识的「载体容量」,但非唯一决定因素 理论上限:参数数量决…...
基于Python的selenium入门超详细教程(第2章)--单元测试框架unittest
学习路线 自动化测试介绍及学习路线-CSDN博客 自动化测试之Web自动化(基于pythonselenium)-CSDN博客 基于Python的selenium入门超详细教程(第1章)--WebDriver API篇-CSDN博客 目录 前言: 一、单元测试 1. 单元测试的定义 2. 单元测…...
日志、类加载器、XML(配置文件)
目录 一、日志1.日志技术的概述2.日志技术的体系a. Logback 3.日志的级别 二、类加载器1.概述2.类加载时机3.类加载过程3.类加载器的分类4.常用方法 三、XML(配置文件)1.概述2.XML的基本语法3.XML的文档约束a.DTD约束b.schema约束 4.XML文档解析a.Dom4jb…...
Flutter中的const和final的区别
目录 一、核心区别对比表 二、初始化机制深度解析 1. const 的编译期特性 2. final 的运行时特性 三、内存管理差异 1. const 的内存优化 2. final 的独立内存 四、集合类型的本质区别 1. const 集合的完全不可变性 2. final 集合的引用不可变性 五、在 Flutter 中的…...
DAY34 贪心算法Ⅲ
134. 加油站 - 力扣(LeetCode) 这种环路问题要记一下。 class Solution { public:int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {int curSum0;int totalSum0;int start0;for(int i0;i<gas.size();i){curSumga…...
AI大白话(一):5分钟了解AI到底是什么?
🌟引言: 在这个信息爆炸的时代,“人工智能”、“AI”、“机器学习”、"深度学习"等词汇频繁出现在我们的生活中。 从手机里的语音助手,到网购平台的个性化推荐,再到最近大火的AI绘画和ChatGPT,人…...
(七)Spring Boot学习——Redis使用
有部分内容是常用的,为了避免每次都查询数据库,将部分数据存入Redis。 一、 下载并安装 Redis Windows 版的 Redis 官方已不再维护,你可以使用 微软提供的 Redis for Windows 版本 或者 使用 WSL(Windows Subsystem for Linux&a…...
蓝桥与力扣刷题(蓝桥 字符统计)
题目:给定一个只包含大写字母的字符出 S, 请你输出其中出现次数最多的字符。如果有多个字母均出现了最多次, 按字母表顺序依次输出所有这些字母。 输入格式 一个只包含大写字母的字等串 S. 输出格式 若干个大写字母,代表答案。 样例输入 BABBACAC样…...
AtCoder Beginner Contest 397(ABCDE)
目录 A - Thermometer 翻译: 思路: 实现: B - Ticket Gate Log 翻译: 思路: 实现: C - Variety Split Easy 翻译: 思路: 实现: D - Cubes 翻译:…...
Profinet转Profinet以创新网关模块为核心搭建西门子和欧姆龙PLC稳定通讯架构案例
你是否有听过PROFINET主站与PROFINET主站之间需要做数据通讯有需求? 例如西门子1500与霍尼韦尔DCS系统两个主站之间的通讯。应用于PROFINET为主站设备还有欧姆龙、基恩士、罗克韦尔、施耐德、GE、ABB等品牌的PLC或DCS、FCS等平台。在生产或智能领域有通讯需求。两头…...
计算机视觉|Swin Transformer:视觉 Transformer 的新方向
一、引言 在计算机视觉领域的发展历程中,卷积神经网络(CNN) 长期占据主导地位。从早期的 LeNet 到后来的 AlexNet、VGGNet、ResNet 等,CNN 在图像分类、目标检测、语义分割等任务中取得了显著成果。然而,CNN 在捕捉全…...
C++单例模式精解
单例模式(重点*) 单例模式是23种常用设计模式中最简单的设计模式之一,它提供了一种创建对象的方式,确保只有单个对象被创建。这个设计模式主要目的是想在整个系统中只能出现类的一个实例,即一个类只有一个对象。 将单…...
【java】集合练习2
Student.java:保存学生类的定义。 public class Student {private String name;private int age;public Student(String name, int age) {this.name name;this.age age;}public String getName() { return name; }public int getAge() { return age; }Overridepu…...
FineBI_实现求当日/月/年回款金额分析
需求:原始数据结构如下,需要在分组表中,实现各城市当日/月/年的合同金额分析 实现步骤: ①维度拖入城市 ②分别取当日/月/年合同金额 当日DEF(SUM_AGG(${ 地区数据分析1 _ 合同金额 }),[${ 地区数据分析1 _ 城市 }],[LEFT(${ 地…...
【计算机网络】2物理层
物理层任务:实现相邻节点之间比特(或)的传输 1.通信基础 1.1.基本概念 1.1.1.信源,信宿,信道,数据,信号 数据通信系统主要划分为信源、信道、信宿三部分。 信源:产生和发送数据的源头。 信宿:接收数据的终点。 信道:信号的传输介质。 数据和信号都有模拟或数字…...
解决PC串流至IPad Pro时由于分辨率不一致导致的黑边问题和鼠标滚轮反转问题
问题背景 今天在做 电脑串流ipad pro 的时候发现了2个问题: 1.ipadpro 接上鼠标后,滚轮上下反转,这个是苹果自己的模拟造成的问题,在设置里选择“触控板与鼠标”。 关闭“自然滚动”,就可以让鼠标滚轮正向滚动。 2. ipadpro 分…...
在办公电脑上本地部署 70b 的 DeepSeek 模型并实现相应功能的大致步骤
以下是为客户在办公电脑上本地部署 70b 的 DeepSeek 模型并实现相应功能的大致步骤: 硬件准备: 70b 模型对硬件要求较高,确保办公电脑有足够强大的 GPU(例如 NVIDIA A100 等高端 GPU,因为模型规模较大,普通…...
LLMs之CoD:《Chain of Draft: Thinking Faster by Writing Less》翻译与解读
LLMs之CoD:《Chain of Draft: Thinking Faster by Writing Less》翻译与解读 导读:这篇论文的核心是提出了一种名为“Chain of Draft”(CoD,草稿链)的新型提示策略,用于改进大型语言模型(LLMs&a…...
Docker安装mysql——Linux系统
拉取mysql镜像 docker pull mysql 查看镜像 docker images 运行镜像(这一步的作用:数据持久化,通过挂载卷将日志、数据和配置文件存储在主机上,避免容器删除导致数据丢失) docker run -p 3306:3306 --name mysql …...
0CTF 2016 piapiapia 1
#源码泄露 #代码审计 #反序列化字符逃逸 #strlen长度过滤数组绕过 www.zip 得到源码 看到这里有flag ,猜测服务端docker的主机里,$flag变量应该存的就是我们要的flag。 于是,我们的目的就是读取config.php 利用思路 这里存在 任意文件读取…...
2、危机应对-核心成员突然退出
一、场景: 当你团队中的骨干突然退出项目,如开发主程不干了,交付经理如何应对? 二、思考: 处理核心成员退出的本质是“通过系统性的减震降低人岗绑定的风险” 三、处理方式: 1、紧急评估影响 技术影响…...
python_巨潮年报pdf下载
目录 前置: 步骤: step one: pip安装必要包,获取年报url列表 step two: 将查看url列表转换为pdf url step three: 多进程下载pdf 前置: 1 了解一些股票的基本面需要看历年年报,在巨潮一个个下载比较费时间&…...
单片机自学指南
一、单片机基础入门 单片机的概念与发展历程 常见单片机类型介绍(如 51 系列、STM32 系列等) 单片机在生活与工业中的应用实例剖析 二、硬件原理学习 单片机内部结构详解(CPU、存储器、I/O 口等) 时钟电路与复位电路原理 电…...
