当,Kotlin Flow与Channel相逢

news2024/12/30 1:46:32

前言

之前的文章已经分析了Flow的相关原理与简单使用,Flow之所以用起来香,Flow便捷的操作符功不可没,而想要熟练使用更复杂的操作符,那么需要厘清Flow和Channel的关系。
本篇文章构成:

image.png

1. Flow与Channel 对比

1.1 Flow核心原理与使用场景

原理

先看最简单的Demo:

    fun test0() {
        runBlocking {
            //构造flow
            val flow = flow {
                //下游
                emit("hello world ${Thread.currentThread()}")
            }
            //收集flow
            flow.collect {
                //下游
                println("collect:$it ${Thread.currentThread()}")
            }
        }
    }

打印结果:

collect:hello world Thread[main,5,main] Thread[main,5,main]

说明下游和上游运行在同一线程里。

image.png

一个最基本的flow包含如下几个元素:

  1. 操作符,也即是函数
  2. 上游,通过构造操作符创建
  3. 下游,通过末端操作符构建

我们可以类比流在管道里流动:
image.png

上游早就准备好了,只是下游没有发出指令,此时上下游是没有建立起关联的,只有当下游渴了,需要水了才会通知上游放水,这个时候上下游才关联起来,管道就建好了。
因此我们认为Flow是冷流。

更多Flow细节请移步:Kotlin Flow啊,你将流向何方?

使用
基于Flow的特性,通常将其用在提供数据的场景,比如生产数据的模块将生产过程封装到flow的上游里,最终创建了flow对象。
而使用数据的模块就可以通过该flow对象去收集上游的数据,如下:

//提供数据的模块
class StudentInfo {
    fun getInfoFlow() : Flow<String> {
        return flow {
            //假装构造数据
            Thread.sleep(2000)
            emit("name=fish age=18")
        }
    }
}

//消费数据的模块
    fun test1() {
        runBlocking {
            val flow = StudentInfo().getInfoFlow()
            flow.collect {
                println("studentInfo:$it")
            }
        }
    }

1.2 Channel核心原理与使用场景

原理
由上可知,Flow比较被动,在没有收集数据之前,上下游是互不感知的,管道并没有建起来。
而现在我们有个场景:

需要将管道提前建起来,在任何时候都可以在上游生产数据,在下游取数据,此时上下游是可以感知的

先看最简单的Demo:

    fun test2() {
        //提前建立通道/管道
        val channel = Channel<String>()
        GlobalScope.launch {
            //上游放数据(放水)
            delay(200)
            val data = "放水啦"
            println("上游:data=$data ${Thread.currentThread()}")
            channel.send(data)
        }

        GlobalScope.launch {
            val data = channel.receive()
            println("下游收到=$data ${Thread.currentThread()}")
        }
    }

image.png

一个最基本的Channel包含如下几个元素:

  1. 创建Channel
  2. 往Channel里放数据(生产)
  3. 从Channel里取数据(消费)

image.png

使用
可以看出与Flow不同的是,生产者、消费者都可以往Channel里存放/取出数据,只是能否进行有效的存放,能否成功取出数据需要根据Channel的状态确定。
Channel最大的特点:

  1. 生产者、消费者访问Channel是线程安全的,也就是说不管生产者和消费者在哪个线程,它们都能安全的存取数据
  2. 数据只能被消费一次,上游发送了1条数据,只要有1个下游消费了数据,则其它下游将不会拿到此数据

更多Channel细节请移步:继续来,同我一起撸Kotlin Channel 深水区

2. Flow与Channel 相逢

2.1 Flow切换线程的始末

思考一种场景:需要在flow里进行耗时操作(比如网络请求),外界拿到flow对象后等待收集数据即可。
很容易我们就想到如下写法:

    fun test3() {
        runBlocking {
            //构造flow
            val flow = flow {
                //下游
                //模拟耗时
                thread { 
                    Thread.sleep(3000)
                    emit("hello world ${Thread.currentThread()}")
                }
            }
        }
    }

可惜的是编译不通过:
image.png
因为emit是挂起函数,需要在协程作用域里调用。

当然,添加一个协程作用域也很简单:

    fun test4() {
        runBlocking {
            //构造flow
            val flow = flow {
                //下游
                val coroutineScope = CoroutineScope(Job() + Dispatchers.IO)
                coroutineScope.launch {
                    //模拟耗时,在子线程执行
                    Thread.sleep(3000)
                    emit("hello world ${Thread.currentThread()}")
                }
            }
            flow.collect {
                println("collect:$it")
            }
        }
    }

编译没有报错,满心欢喜执行,等待3s后,事与愿违:
image.png
意思是"检测到了在另一个线程里发射数据,这种行为不是线程安全的因此被禁止了"。

查看源码发现:
image.png

在emit之前会检测emit所在的协程与collect所在协程是否一致,不一致就抛出异常。
显然在我们上面的Demo里,collect属于runBlocking协程,而emit属于我们新开的协程,当然不一样了。

2.2 ChannelFlow 闪亮登场

2.2.1 自制丐版ChannelFlow

既然是线程安全问题,我们很容易想到使用Channel来解决,在此之前需要对Flow进行封装:

//参数为SendChannel扩展函数
class MyFlow(private val block: suspend SendChannel<String>.() -> Unit) : Flow<String> {
    //构造Channel
    private val channel = Channel<String>()
    override suspend fun collect(collector: FlowCollector<String>) {
        val coroutineScope = CoroutineScope(Job() + Dispatchers.IO)
        coroutineScope.launch {
            //启动协程
            //模拟耗时,在子线程执行
            Thread.sleep(3000)
            //把Channel对象传递出去
            block(channel)
        }

        //获取数据
        val data = channel.receive()
        //发射
        collector.emit(data)
    }
}

如上,重写了Flow的collect函数,当外界调用flow.collect时:

  1. 先启动一个协程
  2. 从channel里读取数据,没有数据则挂起当前协程
  3. 1里的协程执行,调用flow的闭包执行上游逻辑
  4. 拿到数据后进行发射,最终传递到collect的闭包

外界使用flow:

    fun test5() {
        runBlocking {
            //构造flow
            val myFlow = MyFlow {
                send("hello world emit 线程: ${Thread.currentThread()}")
            }

            myFlow.collect {
                println("下游收到=$it collect 线程: ${Thread.currentThread()}")
            }
        }
    }

最终打印:

下游收到=hello world emit 线程: Thread[DefaultDispatcher-worker-1,5,main] collect 线程: Thread[main,5,main]

可以看出,上游、下游在不同的协程里执行,也在不同的线程里执行。
如此一来就满足了需求。

2.2.2 ChannelFlow 核心原理

上面重写的Flow没有使用泛型,也没有对Channel进行关闭,还有其它的点没有完善。
还好官方已经提供了完善的类和操作符,得益于此我们很容易就完成如上需求。

    fun test6() {
        runBlocking {
            //构造flow
            val channelFlow = channelFlow<String> {
                send("hello world emit 线程: ${Thread.currentThread()}")
            }
            channelFlow.collect {
                println("下游收到=$it collect 线程: ${Thread.currentThread()}")
            }
        }
    }

接着来简单分析其原理:

#ChannelFlow.kt
private open class ChannelFlowBuilder<T>(
    //闭包对象
    private val block: suspend ProducerScope<T>.() -> Unit,
    context: CoroutineContext = EmptyCoroutineContext,
    //Channel模式
    capacity: Int = Channel.BUFFERED,
    //Buffer满之后的处理方式,此处是挂起
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
) : ChannelFlow<T>(context, capacity, onBufferOverflow) {
    //...
    override suspend fun collectTo(scope: ProducerScope<T>) =
        //调用闭包
        block(scope)
    //...
}

public abstract class ChannelFlow<T>(
    // upstream context
    @JvmField public val context: CoroutineContext,
    // buffer capacity between upstream and downstream context
    @JvmField public val capacity: Int,
    // buffer overflow strategy
    @JvmField public val onBufferOverflow: BufferOverflow
) : FusibleFlow<T> {
    
    //produceImpl 开启的新协程会调用这
    internal val collectToFun: suspend (ProducerScope<T>) -> Unit
        get() = { collectTo(it) }
    
    public open fun produceImpl(scope: CoroutineScope): ReceiveChannel<T> =
        //创建Channel协程,返回Channel对象
        scope.produce(context, produceCapacity, onBufferOverflow, start = CoroutineStart.ATOMIC, block = collectToFun)

    //重写collect函数
    override suspend fun collect(collector: FlowCollector<T>): Unit =
        //开启协程
        coroutineScope {
            //发射数据
            collector.emitAll(produceImpl(this))
        }
}

produceImpl函数并不耗时,仅仅只是开启了新的协程。
接着来看collector.emitAll:

#Channels.kt
private suspend fun <T> FlowCollector<T>.emitAllImpl(channel: ReceiveChannel<T>, consume: Boolean) {
    ensureActive()
    var cause: Throwable? = null
    try {
        //循环从Channel读取数据
        while (true) {
            //从Channel获取数据
            val result = run { channel.receiveCatching() }
            if (result.isClosed) {
                //如果Channel关闭了,也就是上游关闭了,则退出循环
                result.exceptionOrNull()?.let { throw it }
                break // returns normally when result.closeCause == null
            }
            //发射数据
            emit(result.getOrThrow())
        }
    } catch (e: Throwable) {
        cause = e
        throw e
    } finally {
        //关闭Channel
        if (consume) channel.cancelConsumed(cause)
    }
}

从源码可能无法一眼厘清其流程,老规矩上图就会清晰明了。
image.png

上一小结丐版的实现就是参照channelFlow,若是了解了丐版,再来了解官方豪华版就比较容易。

2.2.3 ChannelFlow 应用场景

查看ChannelFlow衍生的子类:
image.png
这些子类是Flow里各种复杂操作符的基础,如:
buffer、flowOn、flatMapLatest、flatMapMerge等。
因此掌握了ChannelFlow再来看各种操作符就会豁然开朗。

2.3 callbackFlow 拯救你的回调

2.3.1 原理

使用channelFlow {},虽然能够在新的协程里执行闭包,但由于新协程的调度器是使用collect所在协程调度器不够灵活:

    fun test6() {
        runBlocking {
            //构造flow
            val channelFlow = channelFlow<String> {
                send("hello world emit 线程: ${Thread.currentThread()}")
            }
            channelFlow.collect {
                println("下游收到=$it collect 线程: ${Thread.currentThread()}")
            }
        }
    }

collect所在的协程为runBlocking协程,而send函数虽然在新的协程里,但它的协程调度器使用的是collect协程的,因此send函数与collect函数所运行的线程是同一个线程。
虽然我们可以更改外层的调度器使之运行在不同的线程如:

    fun test6() {
        GlobalScope.launch {
            //构造flow
            val channelFlow = channelFlow<String> {
                send("hello world emit 线程: ${Thread.currentThread()}")
            }
            channelFlow.collect {
                println("下游收到=$it collect 线程: ${Thread.currentThread()}")
            }
        }
    }

但终归不灵活,从设计的角度来说,Flow(对象)的提供者并不关心使用者在什么样的环境下进行collect操作。

还是以网络请求为例:

fun getName(callback:NetResult<String>) {
    thread {
        //假装从网络获取
        Thread.sleep(2000)
        callback.onSuc("I'm fish")
    }
}

interface NetResult<T> {
    fun onSuc(t:T)
    fun onFail(err:String)
}

如上,存在这样一个网络请求,在子线程里进行网络请求,并通过回调通知外部调用者。
很典型的一个请求回调,该怎么把它封装为Flow呢?尝试用channelFlow进行封装:

    fun test7() {
        runBlocking {
            //构造flow
            val channelFlow = channelFlow {
                getName(object : NetResult<String> {
                    override fun onSuc(t: String) {
                        println("begin send")
                        trySend("hello world emit 线程: ${Thread.currentThread()}")
                        println("stop send")
                    }
                    override fun onFail(err: String) {
                    }
                })
            }

            channelFlow.collect {
                println("下游收到=$it collect 线程: ${Thread.currentThread()}")
            }
        }
    }

看似美好,实则却收不到数据,明明"begin send"和"stop send"都打印了,为啥collect闭包里没有打印呢?
getName函数内部开启了线程,因此它本身并不是耗时操作,由此可知channelFlow闭包很快就执行完成了。
由ChannelFlow源码可知:CoroutineScope.produce的闭包执行结束后会关闭Channel:
image.png
既然channel都关闭了,当子线程里回调onSuc并执行trySend并不会再往channel发送数据,collect当然就收不到了。

要解决这个问题也很简单:不让协程关闭channel,换句话说只要协程没有结束,那么channel就不会被关闭。而让协程不结束,最直接的方法就是在协程里调用挂起函数。
刚好,官方也提供了相应的挂起函数:

    fun test7() {
        runBlocking {
            //构造flow
            val channelFlow = channelFlow {
                getName(object : NetResult<String> {
                    override fun onSuc(t: String) {
                        println("begin send")
                        trySend("hello world emit 线程: ${Thread.currentThread()}")
                        println("stop send")
                        //关闭channel,触发awaitClose闭包执行
                        close()
                    }
                    override fun onFail(err: String) {
                    }
                })

                //挂起函数
                awaitClose {
                    //走到此,channel关闭
                    println("awaitClose")
                }
            }

            channelFlow.collect {
                println("下游收到=$it collect 线程: ${Thread.currentThread()}")
            }
        }
    }

相较上个Demo而言,增加了2点:

  1. awaitClose 挂起协程,该协程不结束,则channel不被关闭
  2. channel使用完成后需要释放资源,主动调用channel的close函数,该函数最终会触发awaitClose闭包执行,在闭包里做一些释放资源的操作

你可能会说以上用法不太友好,如果不知道有awaitClose这函数,都无法排查为啥没收到数据。
嗯,这官方也考虑到了,那就是callbackFlow。
image.png
可以看出就比channelFlow函数多了个判断:
若是执行了红框部分,说明该协程没有被挂起,则抛出异常提示我们在协程里调用awaitClose函数。

2.3.2 使用

和channelFlow的使用一模一样:

    fun test8() {
        runBlocking {
            //构造flow
            val channelFlow = callbackFlow {
                getName(object : NetResult<String> {
                    override fun onSuc(t: String) {
                        println("begin send")
                        trySend("hello world emit 线程: ${Thread.currentThread()}")
                        println("stop send")
                        //关闭channel,触发awaitClose闭包执行
//                        close()
                    }
                    override fun onFail(err: String) {
                    }
                })

                //挂起函数
                awaitClose {
                    //走到此,channel关闭
                    println("awaitClose")
                }
            }

            channelFlow.collect {
                println("下游收到=$it collect 线程: ${Thread.currentThread()}")
            }
        }
    }

有了callbackFlow,我们就可以优雅的将回调转为Flow提供给外部调用者使用。

3. Flow与Channel 互转

3.1 Channel 转 Flow

Flow和Channel相遇,碰撞出了ChannelFlow,ChannelFlow顾名思义,既是Channel也是Flow,因此可以作为中介对Flow与Channel进行转换。

    fun test9() {
        runBlocking {
            val channel = Channel<String>()
            val flow = channel.receiveAsFlow()
            GlobalScope.launch {
                flow.collect {
                    println("collect:$it")
                }
            }
            delay(200)
            channel.send("hello fish")
        }
    }

channel通过send,flow通过collect收集。

3.2 Flow 转 Channel

    fun test10() {
        runBlocking {
            val flow = flow {
                emit("hello fish")
            }
            val channel = flow.produceIn(this)
            val data = channel.receive()
            println("data:$data")
        }
    }

flow.produceIn(this) 触发collect操作,进而执行flow闭包,emit将数据放到channel里,最后通过channel.receive()取数据。

下篇将完全解析Flow各种操作符,掌握了ChannelFlow再去看操作符相信你会如虎添翼。

本文基于Kotlin 1.5.3,文中完整实验Demo请点击

您若喜欢,请点赞、关注、收藏,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android/Kotlin

1、Android各种Context的前世今生
2、Android DecorView 必知必会
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分发全套服务
6、Android invalidate/postInvalidate/requestLayout 彻底厘清
7、Android Window 如何确定大小/onMeasure()多次执行原因
8、Android事件驱动Handler-Message-Looper解析
9、Android 键盘一招搞定
10、Android 各种坐标彻底明了
11、Android Activity/Window/View 的background
12、Android Activity创建到View的显示过
13、Android IPC 系列
14、Android 存储系列
15、Java 并发系列不再疑惑
16、Java 线程池系列
17、Android Jetpack 前置基础系列
18、Android Jetpack 易学易懂系列
19、Kotlin 轻松入门系列
20、Kotlin 协程系列全面解读

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

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

相关文章

AVL树(C++实现)

文章目录 AVL树的概念AVL树结点定义AVL树的插入AVL树的旋转左单旋右单旋左右单旋右左双旋 AVL树的验证AVL树的性能AVL树及测试完整代码 AVL树的概念 二叉搜索树虽然可以缩短查找的效率,但如果数据有序或接近有序,那么二叉搜索树将退化为单支树,查找元素则相当于在顺序表中搜索…

从零手写Resnet50实战——利用 torch 识别出了虎猫和萨摩耶

大家好啊&#xff0c;我是董董灿。 自从前几天手写了一个慢速卷积之后&#xff08;从零手写Resnet50实战—手写龟速卷积&#xff09;&#xff0c;我便一口气将 Resnet50 中剩下的算法都写完了。 然后&#xff0c;暴力的&#xff0c;按照 Resnet50 的结构&#xff0c;将手写的…

【Flowable】Flowable基础表结构

1.表结构讲解 表结构创建文件&#xff1a;flowable-engine-6.3.0.jar!\org\flowable\db\create\flowable.mysql.create.engine.sql 工作流程的相关操作都是操作存储在对应的表结构中&#xff0c;为了能更好的弄清楚Flowable的实现原理和细节&#xff0c;我们有必要先弄清楚Fl…

Python边缘检测之prewitt, sobel, laplace算子

文章目录 滤波算子简介具体实现测试 滤波算子简介 ndimage中提供了卷积算法&#xff0c;并且建立在卷积之上&#xff0c;提供了三种边缘检测的滤波方案&#xff1a;prewitt, sobel以及laplace。 在convolve中列举了一个用于边缘检测的滤波算子&#xff0c;统一维度后&#xf…

es6 const的使用

1.const用来定义常量&#xff0c;赋值知乎不能再赋值&#xff0c;再次赋值会报错。 <script>//1.定义常量&#xff0c;赋值后不能再赋值&#xff0c;在赋值报错const count 1// count 2</script> ​ 2.const不能只声明不赋值&#xff0c;会报错。 <script>…

智能学习 | MATLAB实现CS-BP多变量时间序列预测(布谷鸟搜索算法优化BP神经网络)

智能学习 | MATLAB实现CS-BP多变量时间序列预测(布谷鸟搜索算法优化BP神经网络) 目录 智能学习 | MATLAB实现CS-BP多变量时间序列预测(布谷鸟搜索算法优化BP神经网络)预测效果基本介绍程序设计参考资料预测效果 基本介绍 MATLAB实现CS-BP多变量时间序列预测(布谷鸟搜索算法…

chatGPT衣食住行10种场景系列教程(01)chatGPT热点事件+开发利器

导读 时隔5个多月&#xff0c;chatGPT可谓是一日千里&#xff0c;越演越火&#xff0c;携带着AIGC行业一起飞了起来&#xff0c;那么在短短5个月当中有那些值得我们关注的事件&#xff1f;有那些好玩的场景&#xff1f;以及有那些chatGPT好用的工具&#xff1f;本文都将一一告…

大数据时代必备技能,学好数据可视化

互联网时代&#xff0c;都在强调数据分析的重要性&#xff0c;但是干巴巴的数据没人爱看&#xff0c;老板们对“简单直观地看数据”的需求愈发强烈。随着大数据建设的如火如荼&#xff0c;别讲底层技术和算法牛逼&#xff0c;最终的效率提升、业绩提升要通过数据展示出来&#…

vba:消息框基础,msgbox

常量常量值说明vbOKOnly0只显示“确定”按钮&#xff08;缺省值&#xff09;VbOKCancel1显示“确定”和“取消”按钮VbAbortRetryIgnore2显示“终止”、“重试”和“忽略” 按钮VbYesNoCancel3显示“是”、“否”和“取消”按钮VbYesNo4显示“是”和“否”按钮VbRetryCancel5显…

Python爬虫实战——获取电影影评

Python爬虫实战——获取电影影评 前言第三方库的安装示例代码效果演示结尾 前言 使用Python爬取指定电影的影评&#xff0c; 注意&#xff1a;本文仅用于学习交流&#xff0c;禁止用于盈利或侵权行为。 操作系统&#xff1a;windows10 家庭版 开发环境&#xff1a;Pycharm Co…

Linux 服务简单优化

硬件优化 处理器&#xff1a;核心数、主频、制程工艺、线程数、缓存等 核心数&#xff1a;1、2、4、6、8、12、24、32等 主频&#xff1a;2.0GHz、2.3GHz等等 制程工艺&#xff1a;22nm、14nm、10nm等等 线程数&#xff1a;1、2 缓存&#xff1a;L1、L2、L3 建议&#xff1a;尽…

OpenHarmony 3.2 Release特性更新简析

1.ArkUI 组件能力增强 支持XComponent控件&#xff0c;可用于EGL/OpenGL ES和媒体数据写入&#xff0c;并在XComponent组件显示&#xff1b;通过XComponent组件&#xff0c;配合NDK能力&#xff0c;构建C/ArkTS混合开发能力&#xff0c;支持游戏、媒体应用开发支持AbilityCom…

前端学习:HTML内联框架

目录 一、HTML Iframe 二、添加iframe的语法 三、Iframe设置高度和宽度 ​编辑 四、Iframe删除边框 五、使用iframe作为链接的目标 六、补充 一、HTML Iframe iframe用于在网页内显示网页。 二、添加iframe的语法 <iframe src"URL"></iframe> 提示…

“计数”排序

目录 一、什么是计数排序&#xff1f;二、如何实现计数排序&#xff1f;三、适用场景四、时间复杂度和空间复杂度 一、什么是计数排序&#xff1f; 计数排序&#xff0c;是通过统计每一个数字出现的次数&#xff0c;并把它映射到与它自己本身数值相同的下标处&#xff0c;再遍…

HoloLens2场景理解,识别平面信息

因为可用的资料比较少,就记录下吧,大家也可以少走弯路,节省时间。 场景理解,通俗的讲,可以识别空间当中的墙面、地板、天花板、平台等. 场景理解&#xff08;Scene Understanding&#xff09;是指 HoloLens2 通过深度传感器、摄像头和计算机视觉算法等技术&#xff0c;能够对…

Centos安装Nvidia驱动解决内核版本不匹配问题

Centos安装Nvidia驱动解决内核版本不匹配问题 问题分析尝试解决 写程序三分钟&#xff0c;配环境三小时&#xff0c;尤其是在一台全新机器/重装系统后。。。 已经解决的&#xff1a; 禁用nouveau驱动并重启电脑&#xff08;参考这篇博客&#xff09;缺少cc&#xff0c;手动yum…

C++---状态压缩dp---炮兵阵地(每日一道算法2023.4.17)

注意事项&#xff1a; 本题为"状态压缩dp—蒙德里安的梦想"和"状态压缩dp—小国王"和"状态压缩dp—玉米田"的近似题&#xff0c;建议先阅读这三篇文章并理解。 题目&#xff1a; 司令部的将军们打算在 NM 的网格地图上部署他们的炮兵部队。 一个…

Pytorch中的仿射变换(F.affine_grid)

目录 1、平移操作实现 2、缩放操作 3、旋转操作 4、转置操作 在pytorch框架中&#xff0c; F.affine_grid 与 F.grid_sample&#xff08;torch.nn.functional as F&#xff09;联合使用来对图像进行变形。 F.affine_grid 根据形变参数产生sampling grid&#xff0c;F.grid_…

深入浅出openGauss的执行器基础

目录 火山模型 Tuple 数据结构设计 条件计算 Expr 和 Var 示例1 filter 示例2 join 示例3 index scan & index only scan 火山模型 执行器各个算子解耦合的基础。对于每个算子来说&#xff0c;只有三步&#xff1a; 1、向自己的孩子拿一个 tuple。即调用孩子节点…

C++初阶之缺省参数

目录 前言 缺省参数 1.缺省参数的概念 2.缺省参数的分类 全缺省参数 半缺省参数 前言 今天小编继续给大家带来C的内容&#xff0c;那么今天小编给大家讲解的就是有关C中缺省参数的介绍。 缺省参数 1.缺省参数的概念 缺省参数是声明或定义函数时为函数的参数指定一个缺省…