Compose 实践与探索十二 —— 附带效应

news2025/3/17 14:29:32

1、SideEffect

Google 官方文档对 side effect 有两种翻译,简体中文翻译为附带效应,繁体中文翻译为副作用。这两个翻译我们用哪个都行,关键是如何理解它的含义。

1.1 什么是副作用

我们在日常生活中听到的副作用大多是医学领域中的,药物的副作用是指除了预期治疗效果外,在使用过程中可能产生的额外不良反应或不良影响。可以理解为,副作用是目标作用为了生效而附带而来的作用,它并不是“负作用”那种完全不好的、负面的作用。

在 Compose 中,副作用(或附带效应,后续出现两个词都认为是 side effect)通常指的是对界面以外的状态进行更改或者操作的行为。在函数式编程中,强调函数的纯粹性,即函数的输出仅依赖于输入,不会对外部状态产生影响。然而,在 UI 开发中,通常需要与外部环境进行交互,比如修改变量、进行网络请求等,这些会导致“副作用”。

比如说:

fun a() {
    var value = 0
    value = 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 = 0
            Column {
                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 = 0
            Column {
                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 = 0
            Column {
                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:

  1. 埋点,统计用户进入以及退出了哪些界面
  2. 组件进入页面时在 DisposableEffect() 内为该组件设置监听器,组件离开页面时在 onDispose() 内取消监听器

此外,我们要看一下 DisposableEffect 的第一个参数 key1:

@Composable
@NonRestartableComposable
fun DisposableEffect(
    key1: Any?,
    effect: DisposableEffectScope.() -> DisposableEffectResult
) {
    remember(key1) { DisposableEffectImpl(effect) }
}

它的作用是,当传入 DisposableEffect() 的 key1 发生变化时,整个 DisposableEffect() 会进行一次重启,重启动作包括两步:

  1. 先对老的 key1 值执行一次离开回调 onDispose()
  2. 然后对新的 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? = null

    override 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.applyCoroutineContext
    remember(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? = null

    override 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 = welcome
    LaunchedEffect(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.applyCoroutineContext
    remember(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 提供最新的坐标数据 newPos
        val 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 值的 Observer
        val 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 的真实数据对象 value
            value = 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() 内任何一个状态发生变化,都会以新值执行一次 collect
                flow.collect { info ->
                    println(info)
                }
            }
        }

在Compose中,副作用通常发生在LaunchedEffectDisposableEffectSideEffect等函数中。这些函数用于处理可能会引起副作用的操作,如启动协程、订阅数据、修改可变状态等。需要注意的是,在Compose中,副作用应该尽量被限制在特定的作用域内,以保持代码的可维护性和可预测性。

总的来说,Compose中的“副作用”指的是对外部状态进行更改或操作的行为,通过合适的方式管理和控制副作用的产生,可以帮助确保应用的正确性和性能。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SSM基础专项复习4——Maven项目管理工具(1)

系列文章 1、SSM基础专项复习1——SSM项目整合-CSDN博客 2、SSM基础专项复习2——Spring 框架&#xff08;1&#xff09;-CSDN博客 3、SSM基础专项复习3——Spring框架&#xff08;2&#xff09;-CSDN博客 文章目录 系列文章 1. Maven 的概念 1.1. 什么是 Maven 1.2. 什…

使用c#进行串口通信

一、串口通信协议 1.串口通信协议简介 串口通信&#xff08;serial communication&#xff09;是一种设备间非常常用的串行通信方式&#xff0c;大部分电子设备都支持&#xff0c;电子工程师再调试设备时也经常使用该通信方式输出调试信息。讲到某一种通信协议&#xff0c;离…

Web开发-PHP应用鉴别修复AI算法流量检测PHP.INI通用过滤内置函数

知识点&#xff1a; 1、安全开发-原生PHP-PHP.INI安全 2、安全开发-原生PHP-全局文件&单函数 3、安全开发-原生PHP-流量检测&AI算法 一、演示案例-WEB开发-修复方案-PHP.INI配置 文章参考&#xff1a; https://www.yisu.com/ask/28100386.html https://blog.csdn.net/…

蓝桥模拟+真题讲解

今天谁一篇文章哈 &#xff01; 由于本篇文章有些的题目只有图片&#xff0c;因此还望各位见谅。 目录 第一题 题目解析 代码原理 代码编写 填空技巧---巧用python 第二题 题目解析 ​编辑 填空技巧---巧用python 第三题 题目链接 题目解析 必备知识 解题技巧 …

C语言【数据结构】:时间复杂度和空间复杂度.详解

引言 详细介绍什么是时间复杂度和空间复杂度。 前言&#xff1a;为什么要学习时间复杂度和空间复杂度 算法在编写成可执行程序后&#xff0c;运行时需要耗费时间资源和空间(内存)资源。因此衡量一个算法的好坏&#xff0c;一般是从时间和空间两个维度来衡量的&#xff0c;即时…

基于Python的selenium入门超详细教程(第2章)--单元测试框架unittest

学习路线 自动化测试介绍及学习路线-CSDN博客 ​自动化测试之Web自动化&#xff08;基于pythonselenium&#xff09;-CSDN博客 基于Python的selenium入门超详细教程(第1章)--WebDriver API篇-CSDN博客 目录 前言&#xff1a; 一、单元测试 1. 单元测试的定义 2. 单元测…

日志、类加载器、XML(配置文件)

目录 一、日志1.日志技术的概述2.日志技术的体系a. Logback 3.日志的级别 二、类加载器1.概述2.类加载时机3.类加载过程3.类加载器的分类4.常用方法 三、XML&#xff08;配置文件&#xff09;1.概述2.XML的基本语法3.XML的文档约束a.DTD约束b.schema约束 4.XML文档解析a.Dom4jb…

AI大白话(一):5分钟了解AI到底是什么?

&#x1f31f;引言&#xff1a; 在这个信息爆炸的时代&#xff0c;“人工智能”、“AI”、“机器学习”、"深度学习"等词汇频繁出现在我们的生活中。 从手机里的语音助手&#xff0c;到网购平台的个性化推荐&#xff0c;再到最近大火的AI绘画和ChatGPT&#xff0c;人…

蓝桥与力扣刷题(蓝桥 字符统计)

题目&#xff1a;给定一个只包含大写字母的字符出 S, 请你输出其中出现次数最多的字符。如果有多个字母均出现了最多次, 按字母表顺序依次输出所有这些字母。 输入格式 一个只包含大写字母的字等串 S. 输出格式 若干个大写字母&#xff0c;代表答案。 样例输入 BABBACAC样…

AtCoder Beginner Contest 397(ABCDE)

目录 A - Thermometer 翻译&#xff1a; 思路&#xff1a; 实现&#xff1a; B - Ticket Gate Log 翻译&#xff1a; 思路&#xff1a; 实现&#xff1a; C - Variety Split Easy 翻译&#xff1a; 思路&#xff1a; 实现&#xff1a; D - Cubes 翻译&#xff1a…

Profinet转Profinet以创新网关模块为核心搭建西门子和欧姆龙PLC稳定通讯架构案例​

你是否有听过PROFINET主站与PROFINET主站之间需要做数据通讯有需求&#xff1f; 例如西门子1500与霍尼韦尔DCS系统两个主站之间的通讯。应用于PROFINET为主站设备还有欧姆龙、基恩士、罗克韦尔、施耐德、GE、ABB等品牌的PLC或DCS、FCS等平台。在生产或智能领域有通讯需求。两头…

计算机视觉|Swin Transformer:视觉 Transformer 的新方向

一、引言 在计算机视觉领域的发展历程中&#xff0c;卷积神经网络&#xff08;CNN&#xff09; 长期占据主导地位。从早期的 LeNet 到后来的 AlexNet、VGGNet、ResNet 等&#xff0c;CNN 在图像分类、目标检测、语义分割等任务中取得了显著成果。然而&#xff0c;CNN 在捕捉全…

C++单例模式精解

单例模式&#xff08;重点*&#xff09; 单例模式是23种常用设计模式中最简单的设计模式之一&#xff0c;它提供了一种创建对象的方式&#xff0c;确保只有单个对象被创建。这个设计模式主要目的是想在整个系统中只能出现类的一个实例&#xff0c;即一个类只有一个对象。 将单…

【java】集合练习2

Student.java&#xff1a;保存学生类的定义。 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_实现求当日/月/年回款金额分析

需求&#xff1a;原始数据结构如下&#xff0c;需要在分组表中&#xff0c;实现各城市当日/月/年的合同金额分析 实现步骤&#xff1a; ①维度拖入城市 ②分别取当日/月/年合同金额 当日DEF(SUM_AGG(${ 地区数据分析1 _ 合同金额 }),[${ 地区数据分析1 _ 城市 }],[LEFT(${ 地…

【计算机网络】2物理层

物理层任务:实现相邻节点之间比特(或)的传输 1.通信基础 1.1.基本概念 1.1.1.信源,信宿,信道,数据,信号 数据通信系统主要划分为信源、信道、信宿三部分。 信源:产生和发送数据的源头。 信宿:接收数据的终点。 信道:信号的传输介质。 数据和信号都有模拟或数字…

解决PC串流至IPad Pro时由于分辨率不一致导致的黑边问题和鼠标滚轮反转问题

问题背景 今天在做 电脑串流ipad pro 的时候发现了2个问题&#xff1a; 1.ipadpro 接上鼠标后&#xff0c;滚轮上下反转&#xff0c;这个是苹果自己的模拟造成的问题&#xff0c;在设置里选择“触控板与鼠标”。 关闭“自然滚动”,就可以让鼠标滚轮正向滚动。 2. ipadpro 分…

LLMs之CoD:《Chain of Draft: Thinking Faster by Writing Less》翻译与解读

LLMs之CoD&#xff1a;《Chain of Draft: Thinking Faster by Writing Less》翻译与解读 导读&#xff1a;这篇论文的核心是提出了一种名为“Chain of Draft”&#xff08;CoD&#xff0c;草稿链&#xff09;的新型提示策略&#xff0c;用于改进大型语言模型&#xff08;LLMs&a…

0CTF 2016 piapiapia 1

#源码泄露 #代码审计 #反序列化字符逃逸 #strlen长度过滤数组绕过 www.zip 得到源码 看到这里有flag &#xff0c;猜测服务端docker的主机里&#xff0c;$flag变量应该存的就是我们要的flag。 于是&#xff0c;我们的目的就是读取config.php 利用思路 这里存在 任意文件读取…

python_巨潮年报pdf下载

目录 前置&#xff1a; 步骤&#xff1a; step one: pip安装必要包&#xff0c;获取年报url列表 step two: 将查看url列表转换为pdf url step three: 多进程下载pdf 前置&#xff1a; 1 了解一些股票的基本面需要看历年年报&#xff0c;在巨潮一个个下载比较费时间&…