Coroutine 基础八 —— Flow 操作符(二)

news2025/1/7 19:15:11

9、异常相关操作符

9.1 try-catch 和 Flow 的异常可见性

Kotlin 官方似乎并不鼓励在 Flow 中通过 try-catch 来捕获异常,而是推荐使用 catch 操作符。

那么这里我们有必要再次对异常管理的观点进行阐述。

我们所说的“异常管理”,在大部分情况下,管理的都是已知异常。对未知异常的管理通常是交给 UncaughtExceptionHandler 来做一个“兜底”的处理。

所谓的已知异常,不是“我知道这里一定会发生异常”,一定会发生的那就不是异常了。异常本来就是正常流程之外的例外流程。对于一个业务而言,异常是除了完美情况下会走的那条通路之外,还可能会走的别的通路。比如网络请求连接超时会收到 TimeoutException,针对这些异常可以去做专门的处理,让整个大流程在发生异常之后,依然可以“正常地”往下走。“正常地”是指,在发生异常时按照设定好的路线继续执行下去。比如连接超时了那就重试或者给用户报错,或者在重试失败后再给用户报错。正确地处理已知的异常,也可以视为“正常”。

为了进一步说明 try-catch 的情况,我们先建立一个在 Flow 中进行网络请求,但是发生超时异常的伪代码:

fun main() = runBlocking<Unit> {
    val scope = CoroutineScope(EmptyCoroutineContext)
    // 模拟上游发送数据
    val flow = flow {
        emit(1)
        emit(2)
        emit(3)
    }
    val job = scope.launch {
        flow.collect {
            // 如果网络连接超时,有可能抛出 TimeoutException
            gitHub.contributors("square", "retrofit")
        }
    }
    job.join()
}

如果只针对某一条数据的异常进行处理,可以在 collect() 内添加 try-catch:

fun main() = runBlocking<Unit> {
    val scope = CoroutineScope(EmptyCoroutineContext)
    // 模拟上游发送数据
    val flow = flow {
        emit(1)
        emit(2)
        emit(3)
    }
    val job = scope.launch {
        flow.collect {
            // 对可能发生异常的代码 try-catch,这属于是对 Flow 中的每一条数据进行检查
            val contributors = try {
                gitHub.contributors("square", "retrofit")
            } catch (e: TimeoutException) {
                // 我们用返回字符串模拟异常处理过程
                "Handle Network error"
            }
            println("Contributors: $contributors")
        }
    }
    job.join()
}

也可以对整个 collect() 加 try-catch:

fun main() = runBlocking<Unit> {
    val scope = CoroutineScope(EmptyCoroutineContext)
    // 模拟上游发送数据
    val flow = flow {
        emit(1)
        emit(2)
        emit(3)
    }
    val job = scope.launch {
        // 对整个 collect() try-catch,进行整体检查
        try {
            flow.collect {
                val contributors = gitHub.contributors("square", "retrofit")
                println("Contributors: $contributors")
            }
        } catch (e: TimeoutException) {
            // 模拟异常处理,实际中可以进行重试或者通知用户
            println("Handle Network error")
        }
    }
    job.join()
}

上述两种情况,在发生 TimeoutException 时都会进入 catch 对异常的处理流程中。

现在,我在使用 flow 发送数据时加 try-catch,注意本意是要包住获取数据可能用到的数据库或网络操作,“顺便”包上了 emit():

fun main() = runBlocking<Unit> {
    val scope = CoroutineScope(EmptyCoroutineContext)
    // 模拟上游发送数据
    val flow = flow {
        try {
            for (i in 1..5) {
                // 生产数据的过程可能包含读数据库、网络请求等操作,因此需要加 try-catch,
                // 只有正常获取数据之后,才能通过 emit() 在最后发送这些数据
                emit(i)
            }

        } catch (e: TimeoutException) {
            println("Error in flow(): $e")
        }
    }

    val job = scope.launch {
        try {
            flow.collect {
                // 对可能发生异常的代码 try-catch,这属于是对 Flow 中的每一条数据进行检查
                val contributors = gitHub.contributors("square", "retrofit")
                throw TimeoutException()
            }
        } catch (e: TimeoutException) {
            // 我们用返回字符串模拟异常处理过程
            println("Handle Network error")
        }
    }
    job.join()
}

运行结果:

Error in flow(): java.util.concurrent.TimeoutException

可以看到,下游抛出的异常,在上游被捕获了,这不是一个合理的结果。

之所以会造成这种现象,要追溯一下源码。collect() 的大括号的内容并不是 collect() 本身,而是指定 collect() 的参数 FlowCollector 接口函数 emit() 的内容:

public interface Flow<out T> {
    public suspend fun collect(collector: FlowCollector<T>)
}

public fun interface FlowCollector<in T> {
    public suspend fun emit(value: T)
}

完整形式相当于:

flow.collect(object : FlowCollector<Int> {
    override suspend fun emit(value: Int) {
        // collect 后 lambda 表达式的内容
    }
})

而这个内容也就是在上游发送数据调用 emit() 时被执行的。所以当上游在 flow 内添加了 try-catch 进行异常捕获时,如果发生了异常,上游的 try-catch 会先于下游在 collect() 之外的 try-catch 捕获到异常。

这样就会产生一个问题:上游的 try-catch 虽然本意是要捕获数据获取过程中的异常,但是由于它包住了 emit(),所以顺便也把下游的数据消费过程的异常也给拦截了。本该由下游捕获的异常被上游拦截了,假如上游和下游的代码是两个程序员写的,那么下游发生异常的信息没有在预期的位置抛出,不仅给调试带来了困难,同时也会因为上游并不关注下游的异常而忽视这个问题,埋下隐患。

协程里有一个 Exception Transparency 的概念,译为异常可见性,说的是上游的 Flow 不应该吞掉下游的异常。因此,上游在使用 try-catch 时,不应该包住 emit():

val flow = flow {
    for (i in 1..5) {
        // 让 try-catch 只包含获取数据操作,而不包住 emit()
        try {
            // 生产数据的过程可能包含读数据库、网络请求等操作...
        } catch (e: TimeoutException) {
            println("Error in flow(): $e")
            throw e
        }
        emit(i)
    }
}

或者使用原来的方式,但是在 catch 中将捕获到的异常原封不动的抛出,以便让下游可以捕获到这个异常:

val flow = flow {
    try {
        for (i in 1..5) {
            emit(i)
        }
    } catch (e: TimeoutException) {
        println("Error in flow(): $e")
        // 将原异常继续抛出,让下游捕获
        throw e
    }
}

两种做法都是为了保证异常的可见性,即为了保证开发者可以拿到他认为可以拿到的异常。

保证异常的可见性只是 Kotlin 对开发者提出的建议,因为保证异常可见性对开发者是有利的。但是遵守这个建议要靠开发者自己,虽然不遵守程序也能运行,但是开发者自己会不方便。

现在进行一下延伸,假如在 Flow 创建后,调用 collect() 之前,使用一些中间操作符,比如 map,考虑两个问题:

  • 下游抛出的异常会经过 map 的代码块吗?
  • map 代码块里抛出的异常最终会走向哪里?

捋一下代码流程:collect() 会触发它的直接上游 map() 进行数据创建,而 map() 只是一个中间商,它所生产的 Flow 会继续触发它的直接上游的 Flow 开始生产数据,也就是在头部的 Flow 中调用 emit() 发射数据。发射数据时,头部 Flow 发射的数据会经过 map() 进行转换处理,再调用其下游的 emit() 发射数据,也就是 collect 代码块的内容:

public inline fun <T, R> Flow<T>.map(crossinline transform: suspend (value: T) -> R): Flow<R> = transform { value ->
    return@transform emit(transform(value))
}

这个 emit() 函数的内容,就是下游 collect 代码块的内容。因此 collect 代码块内抛出异常,会先到达 map() 内的 emit(),然后再向上传递到 map 的上游 Flow 的内。因此,两个问题也有了答案,下游抛出的异常会经过 map 函数(但注意不是 map 后面接的代码块,代码块指定的是参数上的 transform 如果对数据进行转换,而异常是从 map 内部调用的 emit() 抛出的),map 抛出的异常会向上游的 Flow 继续抛出。

假如在中间加一个 transform 操作符,由于 transform 内部也是通过显示调用 emit() 将数据发送到下游,假如对 transform 的 emit() 也加了 try-catch,那么与前面例子中的头部 Flow 一样,它也会拦截下游抛出的异常。因此,我们不能只关注起始的 Flow 中的 emit(),对于其他通过显式调用 emit() 发送数据的操作符,也要注意不要对 emit() 加 try-catch。

结论就一句话,别用 try-catch 包住 emit()。

9.2 catch 操作符

catch() 能捕获上游 Flow 的异常,不会捕获下游(代码块与各种操作符)异常。

catch() 的作用,相当于在 flow {…} 范围内加了 try-catch,但是捕获 flow {…} 内除了 emit() 以外代码抛出的异常。即 catch() 只会捕获上游异常,而不会捕获下游异常。

catch() 也不会捕获 CancellationException,因为这个异常就是用来取消协程的。

先看示例代码:

fun main() = runBlocking<Unit> {
    val scope = CoroutineScope(EmptyCoroutineContext)
    val flow = flow {
        for (i in 1..5) {
            emit(i)
        }
    }.catch { println("catch(): $it") }

    val job = scope.launch {
        try {
            flow.collect {
                val contributors = gitHub.contributors("square", "retrofit")
                throw TimeoutException()
            }
        } catch (e: TimeoutException) {
            println("Handle Network error")
        }
    }
    job.join()
}

运行结果:

Handle Network error

通过结果能看出这个异常是被下游捕获的,而不是 catch() 捕获的。假如,我在上游获取数据时发生了异常:

fun main() = runBlocking<Unit> {
    val scope = CoroutineScope(EmptyCoroutineContext)
    val flow = flow {
        for (i in 1..5) {
            // 模拟获取数据时发生的异常
            throw RuntimeException("flow() error")
            emit(i)
        }
    }.catch { println("catch(): $it") }

    val job = scope.launch {
        try {
            flow.collect {
                val contributors = gitHub.contributors("square", "retrofit")
                throw TimeoutException()
            }
        } catch (e: TimeoutException) {
            println("Handle Network error")
        }
    }
    job.join()
}

那么就只会由 catch() 捕获到这个上游异常:

catch(): java.lang.RuntimeException: flow() error

因为还没到 emit() 发送数据到下游这一步呢,就发生异常并被 catch() 捕获了。

如果有多个 catch(),那么每个 catch() 拿到的就是它上游的异常:第一个 catch() 拿到的是它到起始 Flow 之间发生的异常;第二个 catch() 拿到的是与第一个 catch() 之间发生的异常,以此类推……

什么时候用 catch()?如何在 catch() 与 try-catch 之间做选择?

二者有一个关键区别就是 try-catch 是在 Flow 里面工作的,而 catch() 是在 Flow 之后工作的。这就导致了,如果 Flow 内部发生异常,可以通过 try-catch 直接进行修复,让 Flow “活着”继续进行生产工作;但 catch() 就只能在 Flow 之后接管数据生产操作,因为只有 Flow 内部的异常没有被处理,而是被抛出了,才能流到 catch():

fun main() = runBlocking<Unit> {
    val scope = CoroutineScope(EmptyCoroutineContext)
    // 模拟上游发送数据
    val flow = flow {
        for (i in 1..5) {
            // 假设在 i = 3 时生产数据过程发生了异常,在 Flow 内部可以通过 try-catch 修复
            if (i == 3) {
                try {
                    throw RuntimeException("flow() error")
                } catch (e: Exception) {
                    emit(i)
                }
            } else {
                emit(i)
            }
        }
    }

    val job = scope.launch {
        try {
            flow.collect {
                println("Data: $it")
            }
        } catch (e: RuntimeException) {
            // 我们用返回字符串模拟异常处理过程
            println("Error")
        }
    }
    job.join()
}

假如在 Flow 内部通过 try-catch 修复异常,仍有可能会收到完整的数据:

Data: 1
Data: 2
Data: 3
Data: 4
Data: 5

但假如由于权限问题,我们不能修改 Flow 内部的代码,那么就只能通过 Flow 外接 catch() 来尝试接管数据生产流程:

fun main() = runBlocking<Unit> {
    val scope = CoroutineScope(EmptyCoroutineContext)
    // 模拟上游发送数据
    val flow = flow {
        for (i in 1..5) {
            if (i == 3) {
                throw RuntimeException("flow() error")
            } else {
                emit(i)
            }
        }
    }.catch {
        println("flow() error")
        emit(100)
        emit(200)
        emit(300)
    }

    val job = scope.launch {
        try {
            flow.collect {
                println("Data: $it")
            }
        } catch (e: RuntimeException) {
            // 我们用返回字符串模拟异常处理过程
            println("Error")
        }
    }
    job.join()
}

这样下游能得到一部分原有数据,以及 catch() 接管生产的数据:

Data: 1
Data: 2
flow() error
Data: 100
Data: 200
Data: 300

因此,在二者选择的问题上,优先选择 try-catch,如果因为 Flow 的代码结构问题而无法选择 try-catch,才使用 catch()。结构问题具体是指没有权限修改 Flow 内部代码时,只能在 Flow 外接 catch() 来做一个无奈之下的接管。

但实际情况往往是,catch() 没有办法完美接管上游生产数据的逻辑,所以在 catch() 中通常只能做一些收尾工作。无缝、完美的接管通常是做不到的。

9.3 retry() 与 retryWhen()

retry() 与 retryWhen() 也是针对上游 Flow 的异常的,核心原理与 catch() 相同,区别在于,retry() 与 retryWhen() 在异常时重启上游 Flow。

示例代码:

fun main() = runBlocking<Unit> {
    val scope = CoroutineScope(EmptyCoroutineContext)
    val flow = flow {
        for (i in 1..5) {
            if (i == 3) {
                throw RuntimeException("flow() error")
            } else {
                emit(i)
            }
        }
    }.map { it * 2 }.retry(2) { // it:Throwable
        it is RuntimeException
    }

    val job = scope.launch {
        try {
            flow.collect {
                println("Data: $it")
            }
        } catch (e: RuntimeException) {
            // 我们用返回字符串模拟异常处理过程
            println("Caught RuntimeException!")
        }
    }
    job.join()
}

运行结果:

Data: 2
Data: 4
Data: 2
Data: 4
Data: 2
Data: 4
Caught RuntimeException!

retry() 括号内的参数指的是重试的次数,由于上游在发出 1、2 两条数据后才抛出异常,再加上重试的两次,因此最终会有 3 组数据输出。

此外,retry() {…} 的大括号内指定的是一个 predicate 谓词条件,只有该条件为 true 时 retry() 才会进行重试。如果返回了 false,即使重试次数尚未完成,也不会继续重试,而是将异常向下游抛出。

retryWhen() 相当于是把 retry() 的两个参数合并到一起的版本:

fun main() = runBlocking<Unit> {
    val scope = CoroutineScope(EmptyCoroutineContext)
    val flow = flow {
        for (i in 1..5) {
            if (i == 3) {
                throw RuntimeException("flow() error")
            } else {
                emit(i)
            }
        }
    }.map {
        it * 2
    }.retryWhen { cause, attempt ->
        println("Exception cause: ${cause.message}, attempted time: $attempt")
        // 返回 Boolean 类型的重试条件,为 true 时才重试
        cause is RuntimeException && attempt <= 2
    }

    val job = scope.launch {
        try {
            flow.collect {
                println("Data: $it")
            }
        } catch (e: RuntimeException) {
            // 我们用返回字符串模拟异常处理过程
            println("Caught RuntimeException!")
        }
    }
    job.join()
}

运行结果:

Data: 2
Data: 4
Exception cause: flow() error, attempted time: 0
Data: 2
Data: 4
Exception cause: flow() error, attempted time: 1
Data: 2
Data: 4
Exception cause: flow() error, attempted time: 2
Data: 2
Data: 4
Exception cause: flow() error, attempted time: 3
Caught RuntimeException!

retryWhen() 的两个参数,cause 就是导致异常的 Throwable 对象,而 attempt 是已经尝试过的次数,异常第一次到达 retryWhen() 时,attempt 是 0。由于设置了 attempt <= 2 都可进行重试,因此一共重试三次。

10、全流程监听操作符

其实前面讲的 catch 与 retry 操作符也属于流程监听操作符,只不过异常的处理比较特殊,所以单列出来。接下来几个操作符是对 Flow 的启动和结束的监听。

10.1 onStart 操作符

onStart() 负责监听 Flow 的收集流程的开始事件,执行时机是在 collect() 被调用之后,在正式开始生产数据之前(调用上游的 collect() 之前)。

示例代码:

fun main() = runBlocking<Unit> {
    val scope = CoroutineScope(EmptyCoroutineContext)
    val flow = flow {
        for (i in 1..5) {
            emit(i)
        }
    }.onStart { println("onStart 1") }
        .onStart { println("onStart 2") }

    val job = scope.launch {
        flow.collect {
            println("Data: $it")
        }
    }
    job.join()
}

运行结果:

onStart 2
onStart 1
Data: 1
Data: 2
Data: 3
Data: 4
Data: 5

可以看到,onStart() 是在 collect() 之前运行的,并且下面的 onStart() 先于上面的 onStart() 运行。

由于 onStart() 是在 collect() 之前运行的,因此假如在 onStart() 内抛出了异常,在上游 try-catch 是捕获不到的,只能通过 catch() 捕获:

fun main() = runBlocking<Unit> {
    val scope = CoroutineScope(EmptyCoroutineContext)
    val flow = flow {
        // onStart() 先于这里的代码执行,因此即便对 emit() 使用 try-catch 也捕获不到。
        try {
            for (i in 1..5) {
                emit(i)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }.onStart {
        println("onStart 1")
        throw RuntimeException("onStart error")
    }.onStart {
        println("onStart 2")
    }.catch {
        // 使用 catch() 可以捕获到 onStart() 内抛出的异常
        println("catch: $it")
    }

    val job = scope.launch {
        flow.collect {
            println("Data: $it")
        }
    }
    job.join()
}

运行结果:

onStart 2
onStart 1
catch: java.lang.RuntimeException: onStart error

由于生产过程还没开始就抛出了异常,因此结果中不会打印任何数据。但是这个由 onStart() 抛出的异常确实被 catch() 捕获到了。

10.2 onCompletion 操作符

onCompletion() 监听的是 Flow 的结束,在所有数据发送完毕后触发。除了正常结束,异常结束也会触发 onCompletion():

fun main() = runBlocking<Unit> {
    val scope = CoroutineScope(EmptyCoroutineContext)
    val flow = flow {
        try {
            for (i in 1..5) {
                emit(i)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }.onStart {
        println("onStart 1")
        throw RuntimeException("onStart error")
    }.onStart {
        println("onStart 2")
    }.onCompletion { // it:Throwable? 如果是正常结束,it 就是 null
        println("onCompletion $it")
    }.catch {
        println("catch: $it")
    }

    val job = scope.launch {
        flow.collect {
            println("Data: $it")
        }
    }
    job.join()
}

运行结果:

onStart 2
onStart 1
onCompletion java.lang.RuntimeException: onStart error
catch: java.lang.RuntimeException: onStart error

可以看到,异常结束确实触发了 onCompletion() 的执行,并且这个 onCompletion() 没有拦截异常,catch() 中的内容也打印了。

10.3 onEmpty 操作符

onEmpty() 监听一条数据都没有的情况,其代码块会在 Flow 正常结束且没有发送过一条数据的时候被触发。一定是正常结束才触发,异常结束不会触发。

11、flowOn 操作符

flowOn() 用来定制其上游 Flow 运行的 CoroutineContext 的,大多数使用用来切线程,但是也可以切换其他的 CoroutineContext,如 CoroutineName 等。

先关注如何获取 Flow 的 CoroutineContext:

fun main() = runBlocking<Unit> {
    val scope = CoroutineScope(EmptyCoroutineContext)
    val flow = flow {
        println("CoroutineContext in flow(): $coroutineContext")
        println("CoroutineContext in flow(): ${currentCoroutineContext()}")
        for (i in 1..5) {
            emit(i)
        }
    }

    val job = scope.launch {
        flow.collect {
            println("Data: $it")
        }
    }
    job.join()
}

运行结果:

CoroutineContext in flow(): [BlockingCoroutine{Active}@6108c233, BlockingEventLoop@74ab1442]
CoroutineContext in flow(): [StandaloneCoroutine{Active}@19406503, Dispatchers.Default]
Data: 1
Data: 2
Data: 3
Data: 4
Data: 5

flow.collect() 是在 scope 内调用的,而 scope 用的是 EmptyCoroutineContext,因此其协程上下文应该是 Default。这是因为 Flow 在哪个协程调用的 collect(),它的生产流程就在该协程中启动,也就处于该协程的 CoroutineContext 的上下文环境中。因此,在 Flow 中需要使用 currentCoroutineContext() 才能获取到正确的 CoroutineContext。

再看 flowOn() 的效果,只会对其上游进行切换,不会影响到下游:

fun main() = runBlocking<Unit> {
    val scope = CoroutineScope(EmptyCoroutineContext)
    val flow = flow {
        println("CoroutineContext in flow(): ${currentCoroutineContext()}")
        for (i in 1..5) {
            emit(i)
        }
    }.map {
        println("CoroutineContext in map() 1: ${currentCoroutineContext()}")
        it * 2
    }.flowOn(Dispatchers.IO).map {
        println("CoroutineContext in map() 2: ${currentCoroutineContext()}")
        it * 2
    }

    val job = scope.launch {
        flow.collect {
            println("Data: $it - ${currentCoroutineContext()}")
        }
    }
    job.join()
}

运行结果:

CoroutineContext in flow(): [ProducerCoroutine{Active}@a9dc865, Dispatchers.IO]
CoroutineContext in map() 1: [ProducerCoroutine{Active}@a9dc865, Dispatchers.IO]
CoroutineContext in map() 1: [ProducerCoroutine{Active}@a9dc865, Dispatchers.IO]
CoroutineContext in map() 1: [ProducerCoroutine{Active}@a9dc865, Dispatchers.IO]
CoroutineContext in map() 1: [ProducerCoroutine{Active}@a9dc865, Dispatchers.IO]
CoroutineContext in map() 1: [ProducerCoroutine{Active}@a9dc865, Dispatchers.IO]
CoroutineContext in map() 2: [ScopeCoroutine{Active}@5553375d, Dispatchers.Default]
Data: 4 - [ScopeCoroutine{Active}@5553375d, Dispatchers.Default]
CoroutineContext in map() 2: [ScopeCoroutine{Active}@5553375d, Dispatchers.Default]
Data: 8 - [ScopeCoroutine{Active}@5553375d, Dispatchers.Default]
CoroutineContext in map() 2: [ScopeCoroutine{Active}@5553375d, Dispatchers.Default]
Data: 12 - [ScopeCoroutine{Active}@5553375d, Dispatchers.Default]
CoroutineContext in map() 2: [ScopeCoroutine{Active}@5553375d, Dispatchers.Default]
Data: 16 - [ScopeCoroutine{Active}@5553375d, Dispatchers.Default]
CoroutineContext in map() 2: [ScopeCoroutine{Active}@5553375d, Dispatchers.Default]
Data: 20 - [ScopeCoroutine{Active}@5553375d, Dispatchers.Default]

可以看到 flowOn() 的上游都被切换为 Dispatchers.IO,而下游还都在 Dispatchers.Default 下。

flowOn() 只切换上游的 CoroutineContext,是因为上下游的代码有可能是两个程序员写的。下游通常不会关注上游写了什么,因此如果 flowOn() 连下游的 CoroutineContext 都能切换,会让下游的代码行为变得难以(只写被下游代码的程序员)预期。

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

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

相关文章

NVR小程序接入平台EasyNVR使用FFmpeg取流时提示错误是什么原因呢?

在视频监控系统中&#xff0c;FFmpeg常用于从各种源&#xff08;如摄像头、文件、网络流等&#xff09;获取流媒体数据&#xff0c;这个过程通常称为“取流”。 在EasyNVR平台中&#xff0c;使用FFmpeg取流是一种常见的操作。FFmpeg作为一款强大的开源多媒体处理工具&#xff…

【电源专题】为什么测试电源的SW波形上冲振荡之前的0V电位要先来个小的下降

在同步电源的开关节点SW波形测试中,你可能会发现周期性的SW波形在上升前的一小段时间时间内会有一个小小的下跌,这个下跌会低于0V。那么这个下跌是怎么来的呢? 如下所示为某降压转换器的SW开关节点波形: 其展开后可以看到在上升之前有20ns左右的时间,SW电压是下跌…

基于EB和S32DS3.5建立基础工程

本文参考&#xff1a; https://blog.csdn.net/weixin_41660366/article/details/141949690 https://blog.csdn.net/zhoujingCSDN/article/details/142284796 0、简介 本文基于S32K312 介绍新建工程并引入EB&#xff0c;环境如下&#xff1a; MCU&#xff1a;NXP S32k312 RT…

如何申请LabVIEW软件著作权?

申请 软件著作权 时&#xff0c;若你的单位开发的应用是基于 LabVIEW 的图形化编程语言&#xff0c;你需要将 LabVIEW 程序中的图形化设计转换为源代码形式&#xff0c;以符合软件著作权申请的要求。由于LabVIEW本身是图形化编程语言&#xff0c;而不是传统的文本编程语言&…

C/C++中new/delete与malloc/free的区别及对象管理

C/C++中new/delete与malloc/free的区别及对象管理 在C/C++编程中,动态内存管理是一个核心且复杂的话题,其中new、delete、malloc和free是四个经常用于此目的的工具。尽管它们都涉及到内存的分配和释放,但它们在处理对象时的方式和效果却大相径庭。本文将通过示例来说明这些工…

GitHub 图像修复开源项目推荐【持续更新】

GFPGAN 介绍&#xff1a;GFPGAN&#xff08;Generative Facial Prior-GAN&#xff09;是由腾讯ARC&#xff08;Applied Research Center&#xff09;开发的一种实用的真实世界人脸修复算法。它专门设计用于人脸图像的生成和优化&#xff0c;尤其在低质量人脸图像的超分辨率恢复…

JWT认证实战

JWT&#xff08;JSON Web Token&#xff09;是一种轻量级的、基于 JSON 的开放标准&#xff08;RFC 7519&#xff09;&#xff0c;用于在各方之间安全地传递信息。JWT 的特点是结构简单、轻量化和跨平台支持&#xff0c;适用于用户身份验证、信息加密以及无状态的 API 访问控制…

【无标题】优秀回答统计

在Class-Aware Pseudo-Labeling (CAP) 方法中&#xff0c;类感知阈值&#xff08;Class-Aware Thresholds&#xff09;的动态调整是通过以下步骤实现的&#xff1a; 初始化阈值&#xff1a; 在训练开始时&#xff0c;为每个类别设置初始阈值。这些阈值可以基于先验知识、数据分…

arcgis模版空库怎么用(一)

这里以某个项目的数据为例&#xff1a; 可以看到&#xff0c;属性表中全部只有列标题&#xff0c;无数据内容 可能有些人会认为空库是用来往里面加入信息的&#xff0c;其实不是&#xff0c;正确的用法如下&#xff1a; 一、下图是我演示用的数据&#xff0c;我们可以看到其中…

基于深度学习的视觉检测小项目(六) 项目的信号和变量的规划

• 关于前后端分离 当前流行的一种常见的前后端分离模式是vueflask&#xff0c;vueflask模式的前端和后端之间进行数据的传递通常是借助 API&#xff08;应用程序编程接口&#xff09;来完成的。vue通过调用后端提供的 API 来获取或提交数据。例如&#xff0c;前端可能通过发送…

edeg插件/扩展推荐:助力生活工作

WeTab 此插件在我看来有2个作用 1.改变edeg的主页布局和样式,使其更加精简,无广告 2.提供付费webtab Ai(底层是chatGpt) 沉浸式翻译 此插件可翻译网页的内容 假设我们浏览github 翻译前 翻译后 Better Ruler 可以对网页的距离进行测量 适合写前端的小伙伴 用法示例:

k8s基础(4)—Kubernetes-Service

Service概述 抽象层 ‌k8s的Service是一种抽象层&#xff0c;用于为一组具有相同功能的Pod提供一个统一的入口地址&#xff0c;并通过负载均衡将网络流量分发到这些Pod上。‌ Service解决了Pod动态变化的问题&#xff0c;例如Pod的IP地址和端口可能会发生变化&#xff0c;通过…

客户案例:基于慧集通(DataLinkX)集成平台的金蝶云星空公有云与WMS系统对接集成方案

本文档详细介绍了基于慧集通&#xff08;DataLinkX&#xff09;集成平台的金蝶云星空公有云与WMS系统对接集成方案。该方案旨在实现金蝶云星空与WMS系统之间的数据同步和流程对接&#xff0c;以提高企业供应链管理的效率和准确性。通过物料、供应商资料同步&#xff0c;采购、销…

jenkins入门4 --window执行execute shell

1、启动关闭jenkins 在Windows环境下&#xff0c;如果你需要关闭Jenkins服务&#xff0c;可以通过以下几种方式&#xff1a; 1、使用Windows服务管理器&#xff1a; 打开“运行”对话框&#xff08;Win R&#xff09;&#xff0c;输入services.msc&#xff0c;然后回车。 在服…

ZYNQ初识7(zynq_7010)RAM_IP核

学习汇总正点原子bi站教学视频。但由于目前的学习板PL端缺乏时钟晶振&#xff0c;所以需要从PS端调用时钟供给PL端使用&#xff0c;也就造成顶层文件的设置出现一些问题&#xff0c;在IP核创建调用和例化过程中一些功能会受到限制&#xff0c;所以以下仅作汇总参考。 zynq_7000…

LeetCode:98.验证二叉搜索树

跟着carl学算法&#xff0c;本系列博客仅做个人记录&#xff0c;建议大家都去看carl本人的博客&#xff0c;写的真的很好的&#xff01; 代码随想录 LeetCode&#xff1a;98.验证二叉搜索树 给你一个二叉树的根节点 root &#xff0c;判断其是否是一个有效的二叉搜索树。 有效 …

Golang:使用minio替代文件系统实战教程

本文讨论项目开发中直接文件系统的限制或不足&#xff0c;接着介绍Minio对象存储的优势。同时给出Golang的实际示例代码&#xff0c;包括初始化客户端、读取minio对象以及设置过期策略等。 文件系统 vs Minio 在开发的早期阶段&#xff0c;常见的做法是使用文件系统来存储和检…

拥抱时代--AI(3)

python语言为了研究机器学习专门发展起来一套框架&#xff0c;并且这个框架是开源的&#xff0c;它就是scikit-learn。它主要实现数据预处理&#xff0c;分类&#xff0c;回归&#xff0c;降维&#xff0c;模型选择等最常用的机器学习算法。 在使用scikit-learn之前&#xff0…

实现多账户cursor限制的免费使用

目录 前言 个人建议&#xff1a; 准备工作 下载&#xff1a; 打开cursor&#xff1a; 打开下载文件目录&#xff1a…

OpenCV计算机视觉 05 图像边缘检测(Sobel算子、Scharr算子、Laplacian算子、Canny边缘检测)

图像边缘检测 边缘检测是图形图像处理、计算机视觉和机器视觉中的一个基本工具&#xff0c;通常用于特征提取和特征检测&#xff0c;旨在检测一张数字图像中有明显变化的边缘或者不连续的区域。 yuancv2.imread(yuan.png) cv2.imshow(yuan,yuan) cv2.waitKey(0) yuan_xcv2.Sob…