返回顶部
首页 > 资讯 > 移动开发 >KotlinFlow操作符及基本使用详解
  • 804
分享到

KotlinFlow操作符及基本使用详解

2024-04-02 19:04:59 804人浏览 八月长安
摘要

目录一、Flow的基本概念二、Flow的生命周期与异常处理2.1 开始与结束2.2 异常的处理2.3 retry的处理2.4 超时的处理2.5 Flow的取消三、Flow的创建方式四

一、Flow的基本概念

Kotlin 的 Flow 相信大家都或多或少使用过,毕竟目前比较火,目前我把Flow的使用整理了一下。希望和大家所学对照一下,能有所启发。

Kotlin 的 Flow 用于流式编程,作用于 Kotlin 的协程内。与协程的生命周期绑定,当协程取消的时候,Flow 也会取消。

Flow 的操作符和 RxJava 类似,如果之前会 RxJava 那么可以轻松上手,相比 RxJava ,Flow 更加的简单与场景化。

按照 Flow 的数据流顺序发送的过程,我们对数据流的三个角色交提供方(创建),中介(转换),使用方(接收)。

按照 Flow 流 是否由接收者开始接收触发整个流的启动,我们分为冷流(由接收方启动)与热流(不由接收方启动)。

基本的使用:

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
   runBlocking {
        flow {
            for (i in 1..5) {
                delay(100)
                emit(i)
            }
        }.collect {
            println(it)
        }
    }

打印结果:

基本的使用默认是冷流,从 collect 方法开始发送数据,这里简单的定义一个创建者和一个接收者。

二、Flow的生命周期与异常处理

和 RxJava 一样 ,我们同样的可以监听任务流的生命周期,开始,结束,与接收

2.1 开始与结束

      runBlocking {
            flow {
                for (i in 1..5) {
                    delay(100)
                    emit(i)
                }
            }.onStart {
                YYLogUtils.w("onStart")
            }.onCompletion { exception ->
                YYLogUtils.w("onCompletion: $exception")
            }.collect { v ->
                YYLogUtils.w(v.toString())
            }
        }

打印结果:

2.2 异常的处理

假如如果我们手动的抛一个异常,看看日志的打印顺序

        val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
            YYLogUtils.e(throwable.message ?: "Unkown Error")
        }
        lifecycleScope.launch(exceptionHandler) {
            flow {
                for (i in 1..5) {
                    delay(100)
                    emit(i)
                    if (i == 3) throw RuntimeException("自定义错误")
                }
            }.onStart {
                YYLogUtils.w("onStart")
            }.onCompletion { exception ->
                YYLogUtils.w("onCompletion: $exception")
            }.collect { v ->
                YYLogUtils.w(v.toString())
            }
        }

看到打印结果:

我们可以在 onCompletion 中打印出错误的对象。但是大家看到我这里使用的协程的上下文来捕获的异常,因为如果不捕获这个异常,程序会崩溃,那可不可以不使用协程的异常捕获?可以,我们可以使用 Flow 的异常捕获。

        runBlocking {
            flow {
                for (i in 1..5) {
                    delay(100)
                    emit(i)
                    if (i == 3) throw RuntimeException("自定义错误")
                }
            }.onStart {
                YYLogUtils.w("onStart")
            }.onCompletion { exception ->
                YYLogUtils.w("onCompletion: $exception")
            }.catch { exception ->
                YYLogUtils.e("catch: $exception")
            }.collect { v ->
                YYLogUtils.w(v.toString())
            }
        }

可以看到打印的顺序是和上面的一样的,只是换成了Flow来捕获异常了

2.3 retry的处理

retry 方法可以让我们程序在错误或异常的时候尝试重新执行创建者的操作。

       runBlocking {
            flow {
                for (i in 1..5) {
                    delay(100)
                    emit(i)
                    if (i == 3) throw RuntimeException("自定义错误")
                }
            }.retry(2).onStart {
                YYLogUtils.w("onStart")
            }.onCompletion { exception ->
                YYLogUtils.w("onCompletion: $exception")
            }.catch { exception ->
                YYLogUtils.e("catch: $exception")
            }.collect { v ->
                YYLogUtils.w(v.toString())
            }
        }

可以看到打印的结果:

只会走一次开始的回调和结束的回调。

2.4 超时的处理

measureTimeMillis { }withTimeout(xx) { } 这些都是协程内部的快捷处理方法,处理时间相关。

withTimeout 超时的处理使用的时候协程的快捷函数,其实跟Flow没什么关系,并不是Flow包下面的,所以我们不能使用Flow的异常来处理,因为它是Flow的父亲协程里面的错误,我们可以使用try-catch。也可以使用协程上下文来处理异常。

       val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
            YYLogUtils.e(throwable.message ?: "Unkown Error")
        }
        lifecycleScope.launch(exceptionHandler) {
            withTimeout(200) {
                flow {
                    for (i in 1..5) {
                        delay(100)
                        emit(i)
                        if (i == 3) throw RuntimeException("自定义错误")
                    }
                }.retry(2).onStart {
                    YYLogUtils.w("onStart")
                }.onCompletion { exception ->
                    YYLogUtils.w("onCompletion: $exception")
                }.catch { exception ->
                    YYLogUtils.e("catch: $exception")
                }.collect { v ->
                    YYLogUtils.w(v.toString())
                }
            }
        }

打印结果如下:

如果我想计算Flow的耗时,其实就是计算协程内的一个任务的耗时,跟是不是Flow没关系。

       lifecycleScope.launch(exceptionHandler) {
            val time = measureTimeMillis {
                flow {
                    for (i in 1..5) {
                        delay(100)
                        emit(i)
                    }
                }.onStart {
                    YYLogUtils.w("onStart")
                }.onCompletion { exception ->
                    YYLogUtils.w("onCompletion: $exception")
                }.catch { exception ->
                    YYLogUtils.e("catch: $exception")
                }.collect { v ->
                    YYLogUtils.w(v.toString())
                }
            }
            YYLogUtils.w("总耗时time:" + time)
        }

打印的结果:

2.5 Flow的取消

同样的 Flow 并没有提供取消的方法,因为Flow是运行在协程中的我们可以依赖协程的运行与取消来实现 Flow 的取消。

我们知道默认的flow是冷流 ,flow.collect 才是触发点,它标记为 suspend 函数,需要执行在协程中。我们就能把Flow的创建和接收分开,并且取消 flow.collect 的作用域协程。

       runBlocking {
            val flow = flow {
                for (i in 1..5) {
                    delay(100)
                    emit(i)
                }
            }.onStart {
                YYLogUtils.w("onStart")
            }.onCompletion { exception ->
                YYLogUtils.w("onCompletion: $exception")
            }.catch { exception ->
                YYLogUtils.e("catch: $exception")
            }
            //Flow的取消依赖协程的取消
            withTimeoutOrNull(210) {
                flow.collect {
                    YYLogUtils.w("collect:$it")
                }
            }
        }

效果如下:

甚至我们Flow的创建一般都不需要在协程中,比如我还能这么改

     val flow = (1..5).asFlow().onStart {
            YYLogUtils.w("onStart")
        }.onCompletion { exception ->
            YYLogUtils.w("onCompletion: $exception")
        }.catch { exception ->
            YYLogUtils.e("catch: $exception")
        }
        runBlocking {
            //Flow的取消依赖协程的取消
            withTimeoutOrNull(210) {
                flow.collect {
                    YYLogUtils.w("collect:$it")
                }
            }
        }

这里先讲到Flow的基本生命周期与异常处理,Flow的超时,计时,取消等概念,下面我们看看Flow的创建的方式

三、Flow的创建方式

上面的代码中我们可以看到Flow的创建有几种方式,这里总结一下

flow:创建Flow的操作符。 flowof:构造一组数据的Flow进行发送。 asFlow:将其他数据转换成Flow,一般是其他数据格式向Flow的转换

flow构建器 是经常被使用的流构建器,emit 是 suspend 的需要在协程中执行

flow {
    for (i in 1..5) {
        delay(100)
        emit(i)
    }
}

flowOf构建器 可以用于定义能够发射固定数量值的流

flowOf(1,2,3,4,5)

asFlow构建器可以将各种集合转为Flow 例如 LongRange IntRange IntArray Array Sequence

(1..10).asFlow()

LiveData同样可以使用 asFlow 扩展方法转换为flow

  val liveData = MutableLiveData<Int>(1)
        val flow = liveData.asFlow()
            .onStart {
                YYLogUtils.w("onStart")
            }.onCompletion { exception ->
                YYLogUtils.w("onCompletion: $exception")
            }.catch { exception ->
                YYLogUtils.e("catch: $exception")
            }
        runBlocking {
            //Flow的取消依赖协程的取消
            withTimeoutOrNull(210) {
                flow.collect {
                    YYLogUtils.w("collect:$it")
                }
            }
        }

四、Flow的接收方式

Flow的接收函数操作符常见的有

  • collect、数据收集操作符,默认的flow是冷流,即当执行collect时,上游才会被触发执行。

  • collectIndexed、带下标的收集操作,如collectIndexed{ index, value -> }
  • collectLatest、与collect的区别:当新值从上游发出时,如果上个收集还未完成,会取消上个值得收集操作
  • toList、toSet等 将flow{}结果转化为集合。
  • single 确保流发射单个值
  • first 仅仅取第一个值
  • reduce 如果发射的是 Int,最终会得到一个 Int,可做累加操作
  • fold reduce 的升级版,多了一个初始值

其他的终端操作符,大家看名字都知道怎么使用了,我就不一一演示了,这里看看后面2个 reduce 怎么使用的。

       runBlocking {
            val reduce = flowOf(1, 2, 3).reduce { accumulator, value ->
                YYLogUtils.w("accumulator:" + accumulator + " value:" + value)
                accumulator + value
            }
            YYLogUtils.w("reduce:" + reduce)
        }

看Log可以看到 accumulator 是已经加过的数值 value是当前数值

那么fold和 reduce类似,只是多了一个初始值

        runBlocking {
            val fold = flowOf(1, 2, 3).fold(4) { accumulator, value ->
                YYLogUtils.w("accumulator: $accumulator value: $value")
                accumulator + value
            }
            println(fold)
            YYLogUtils.w("fold:" + fold)
        }

比较难理解的就是这2个了。

五、Flow的转换操作符

我们除了Flow创建于接收之外的一些操作符,另外都是一些中间操作符,要说起flow操作符可太多了,这里只说下常用的几个操作符吧

5.1 基本操作符

transform transfORM 它会调用 Flow 的 collect() 函数,然后构建一个新的 Flow. 如果想要继续发射值,需要重新调用 emit() 函数。

runBlocking {
    flowOf(1, 2, 3).transform {
        emit("transformed $it")
    }.collect {
        println("Collect: $it")
    }
}

map实际上就是 transform + emit ,自动封装了

runBlocking {
    flowOf(1, 2, 3).map {
        "mapped $it"
    }.collect {
        println("Collect: $it")
    }
}

drop 根据条件不要表达式内的数据

runBlocking {
    flowOf(1, 2, 3).dropWhile {
        it < 2
    }.collect {
        println("Collect $it")
    }
}

打印值为: 2 3

filter 根据条件只要表达式内的数据

runBlocking {
    flowOf(1, 2, 3).filter {
        it < 2
    }.collect {
        println("Collect $it")
    }
}

打印值为: 1

debounce 防抖动函数,当用户在很短的时间内输入 “d”,“dh”,“dhl”,但是用户可能只对 “dhl” 的搜索结果感兴趣,因此我们必须舍弃 “d”,“dh” 过滤掉不需要的请求,针对于这个情况,我们可以使用 debounce 函数,在指定时间内出现多个字符串,debounce 始终只会发出最后一个字符串

runBlocking {
   val result = flow {
    emit("h")
    emit("i")
    emit("d")
    delay(90)
    emit("dh")
    emit("dhl")
}.debounce(200).toList()
println(result)
}

打印值为:dhl

distinctUntilChanged 用来过滤掉重复的请求,只有当前值与最后一个值不同时才将其发出

runBlocking {
   val result = flow {
    emit("d")
    emit("d")
    emit("d")
    emit("d")
    emit("dhl")
    emit("dhl")
    emit("dhl")
    emit("dhl")
}.distinctUntilChanged().toList()
println(result)
}

打印值为:d, dhl

flatMapLatest当有新值发送时,会取消掉之前还未转换完成的值

runBlocking {
flow {
    emit("dh")
    emit("dhl")
}.flatMapLatest { value ->
    flow<String> {
        delay(100)
        println("collected $value") // 最后输出 collected dhl
    }
}.collect()
}

场景如下,正在查询 “dh”,然后用户输入 “dhl”

打印值为:dhl

5.2 特殊操作符

这里涉及到上下游数据耗时的问题,类似RxJava的背压的概念,比如发出的数据的速度比较快,但是接受的数据的速度比较慢。场景如下

        runBlocking {
            val time = measureTimeMillis {
                flow {
                    (1..5).forEach {
                        delay(200)
                        println("emit: $it, ${System.currentTimeMillis()}, ${Thread.currentThread().name}")
                        emit(it)
                    }
                }.collect {
                    delay(500)
                    println("Collect $it, ${System.currentTimeMillis()}, ${Thread.currentThread().name}")
                }
            }
            println("time: $time")
        }

那么就会阻塞掉,5*700 = 3500

可以看到上面的结果是串行等待执行的,那么我们可以通过下面的操作符让代码并发执行优化效率

buffer 该运算符会在执行期间为流创建一个单独的协程。从而实现并发效果。

        runBlocking {
            val time = measureTimeMillis {
                flow {
                    (1..5).forEach {
                        delay(200)
                        println("emit: $it, ${System.currentTimeMillis()}, ${Thread.currentThread().name}")
                        emit(it)
                    }
                }.buffer().collect {
                    delay(500)
                    println("Collect $it, ${System.currentTimeMillis()}, ${Thread.currentThread().name}")
                }
            }
            println("time: $time")
        }

可以看到是并发执行的。时间是200 + 5*500 = 2700

conflate 发射数据太快,只处理最新发射的

        runBlocking {
            val time = measureTimeMillis {
                flow {
                    (1..5).forEach {
                        delay(200)
                        println("emit: $it, ${System.currentTimeMillis()}, ${Thread.currentThread().name}")
                        emit(it)
                    }
                }.conflate().collect {
                    delay(500)
                    println("Collect $it, ${System.currentTimeMillis()}, ${Thread.currentThread().name}")
                }
            }
            println("time: $time")
        }

注意会丢失数据的,2和4没有接收到

collectLatest 接收处理太慢,只处理最新接收的,但是注意一下这个不是中间操作符,这个是接收操作符,这里作为对比在这里演示

        runBlocking {
            val time = measureTimeMillis {
                flow {
                    (1..5).forEach {
                        delay(200)
                        println("emit: $it, ${System.currentTimeMillis()}, ${Thread.currentThread().name}")
                        emit(it)
                    }
                }.collectLatest {
                    // 消费效率较低
                    delay(500)
                    println("Collect $it, ${System.currentTimeMillis()}, ${Thread.currentThread().name}")
                }
            }
            println("time: $time")
        }

这样就只会接收到最后一个 5

5.3 组合与展平操作符

zip 组合两个流,将2个Flow合并为1个Flow

runBlocking {
    flowOf("a", "b", "c").zip(flowOf(1, 2, 3)) { a, b -&gt;
        a + b  //自己定义规则
    }.collect {
        println(it)
    }
}

打印 a1 b2 c3

combine 可以合并多个不同的 Flow 数据流,生成一个新的流。只要其中某个子 Flow 数据流有产生新数据的时候,就会触发 combine 操作,进行重新计算,生成一个新的数据。

      val bannerFlow = MutableStateFlow<String?>(null)
        val listFlow = MutableStateFlow<String?>(null)
        lifecycleScope.launch {
            combine(bannerFlow, listFlow) { banner, list ->
                val resultList = mutableListOf<String?>()
                if (banner != null) {
                    resultList.add(banner)
                }
                if (list != null) {
                    resultList.add(list)
                }
                return@combine resultList
            }.collect { list ->
                YYLogUtils.w("list:" + list)
            }
            withContext(Dispatchers.Default) {
                delay(1000)
                bannerFlow.emit("Banner")
            }
            withContext(Dispatchers.Default) {
                delay(3000)
                listFlow.emit("list")
            }
        }

可以看到只要其中一个Flow更新了数据都会刷新

对比zip与combine 同样的代码我们对比zip与combine的区别,注意看Log的打印

        val bannerFlow = MutableStateFlow<String?>(null)
        val listFlow = MutableStateFlow<String?>(null)
        lifecycleScope.launch {
            bannerFlow.zip(listFlow){ banner, list ->
                val resultList = mutableListOf<String?>()
                if (banner != null) {
                    resultList.add(banner)
                }
                if (list != null) {
                    resultList.add(list)
                }
                return@zip resultList
            }.collect { list ->
                YYLogUtils.w("list:" + list)
            }
        }
        lifecycleScope.launch {
            withContext(Dispatchers.Default) {
                delay(1000)
                bannerFlow.emit("Banner")
            }
            withContext(Dispatchers.Default) {
                delay(3000)
                listFlow.emit("list")
            }
        }

zip只有在都更新了才会触发,这也是最重要的他们的不同点

merge

merge 操作符想说 活都被你们干完了,我还活不活了!??

其实我们可以理解 同一类型我们可以用 merge ,不同的类型我们用zip、combine 。 zip、combine需要我们自定义拼接方式,而 merge 则是需要两者类型一样,直接合并为一个对象。

       val bannerFlow = MutableStateFlow<String?>(null)
        val listFlow = MutableStateFlow<String?>(null)
        lifecycleScope.launch {
            listOf(bannerFlow, listFlow).merge().collect {
                YYLogUtils.w("value:$it")
            }
        }
        lifecycleScope.launch {
            withContext(Dispatchers.Default) {
                delay(1000)
                bannerFlow.emit("Banner")
            }
            withContext(Dispatchers.Default) {
                delay(3000)
                listFlow.emit("list")
            }
        }

打印结果:

展平操作符 flattenConcat 以顺序方式将给定的流展开为单个流

    runBlocking {
        flow {
            emit(flowOf(1, 2))
            emit(flowOf(3,4))
        } .flattenConcat().collect { value->
            print(value)
        }
    }

执行结果:1 2 3 4

flattenMerge 作用和 flattenConcat 一样,但是可以设置并发收集流的数量

    runBlocking {
        flow {
            emit(flowOf(1, 2))
            emit(flowOf(3,4))
        } .flattenMerge(2).collect { value->
            print(value)
        }
    }

执行结果:1 2 3 4

flatMapConcat通过展平操作,用这两个元素各自构建出一个新的流 it + "一年级", it + "二年级", it + "三年级"。

runBlocking {
    flowOf("初中", "高中").flatMapConcat {
        flowOf(it + "一年级", it + "二年级", it + "三年级")
    }.collect {
        YYLogUtils.w(it)
    }
}

flatMapMerge

runBlocking {
    flowOf("初中", "高中").flatMapMerge {
        flowOf(it + "一年级", it + "二年级", it + "三年级")
    }.collect {
        YYLogUtils.w(it)
    }
}

实现的效果是一样的,和上面的 flatten 效果类似,后缀为 Concat 是串行,后缀为 Merge 的是并行,效率更高。

5.4 切换线程

Flow的切换线程相比协程的更加简单,至于使用的方式大家可能都不陌生了,这里我简单的举例。

切换线程的操作分一个中间操作符和一个接收操作符,代码如下:

        flow {
            YYLogUtils.w( "start: ${Thread.currentThread().name}")
            repeat(3) {
                delay(1000)
                this.emit(it)
            }
            YYLogUtils.w( "end: ${Thread.currentThread().name}")
        }
            .flowOn(Dispatchers.Main)
            .onEach {
                YYLogUtils.w( "collect: $it, ${Thread.currentThread().name}")
            }
            .launchIn(CoroutineScope(Dispatchers.IO))

打印结果如下:

launchIn为接收操作符,指明此Flow运行的协程作用域对象,一般我们可以指定作用域,指定运行线程,例如CoroutineScope(Dispatchers.IO)、 MainScope() 、lifecycleScope等。

flowOn则是切换线程,需要指明协程的上下文对象 ,一般我们用于切换线程。

总结

不知不觉文章又超长了,内容有点太多了,下面总结一下。

总的来说以上应该都是 Flow 使用的基础了,看下来感觉和RxJava还是很像的吧,我们使用 Kotlin 构建项目的过程中,我们真心是不需要 RxJava 了,基本上 Flow 能替代 RxJava 完成我们想要的效果了。毕竟导入一个 RxJava 的库也不小。

Flow 的使用还是很常见的,同时还有它的一些封装类 SharedFlow 与 StateFlow 也比较常用。

以上就是Kotlin Flow操作符及基本使用详解的详细内容,更多关于Kotlin Flow操作符的资料请关注编程网其它相关文章!

--结束END--

本文标题: KotlinFlow操作符及基本使用详解

本文链接: https://lsjlt.com/news/166477.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • KotlinFlow操作符及基本使用详解
    目录一、Flow的基本概念二、Flow的生命周期与异常处理2.1 开始与结束2.2 异常的处理2.3 retry的处理2.4 超时的处理2.5 Flow的取消三、Flow的创建方式四...
    99+
    2024-04-02
  • Qt QTableWidget基本操作及使用
    目录界面设计与初始化QTableWidget 基本操作设置表头初始化表格数据获得当前单元格数据插入、添加、删除行其他属性控制遍历表格读取数据QTableWidget 是 Qt 中的表...
    99+
    2024-04-02
  • 【MySQL基础】MySQL基本操作详解
    系列文章目录 第1篇:【MySQL基础】MySQL介绍及安装 第2篇:【MySQL基础】MySQL基本操作详解 文章目录 ✍1,数据库操作     🔍1.1,查看数据库     🔍1.2,创建数据库    ...
    99+
    2023-08-16
    mysql 数据库 服务器
  • 一文详解Java中字符串的基本操作
    目录一、遍历字符串案例二、统计字符次数案例三、字符串拼接案例四、字符串反转案例五、帮助文档查看String常用方法一、遍历字符串案例 需求:键盘录入一个字符串,使用程序实现在控制台遍...
    99+
    2024-04-02
  • 详解mysql基本操作详细(二)
    前言 本文类容 1、数据库的几大约束 2、表与表之间的关系 约束: 主键约束: 作用:为了保证数据的有效性和完整性 mysql中常用的约束:主键约束(primary key) 唯一约束(unique) ...
    99+
    2024-04-02
  • KotlinFlow常用封装类StateFlow使用详解
    目录Kotlin中StateFlow的使用一、StateFlow的使用二、替代LiveData总结Kotlin中StateFlow的使用 StateFlow 是 Flow 的实现,是...
    99+
    2024-04-02
  • 详解C#winformListView的基本操作
    组图的加载与导入 图片存放的相对路径/ 与exe存放在一个文件夹 为界面添加图片组–组件 图片下载路径链接:img_jb51.rar // 组图的加载与导入 Di...
    99+
    2024-04-02
  • python字符串基础操作详解
    目录字符串的赋值单引号字符串赋值给变量双引号字符串赋值给变量三引号字符串赋值给变量(多行)字符串的截取截取指定位置的字符获取指定位置之后的所有字符截取指定位置之前的所有字符获取所有的...
    99+
    2024-04-02
  • 详解Qt使用QImage类实现图像基本操作
    目录一、项目介绍二、项目基本配置三、UI界面设计四、主程序实现4.1 widget.h头文件4.2 widget.cpp源文件五、效果演示一、项目介绍 利用QImage类实现对图像的...
    99+
    2024-04-02
  • Redis数据库安装部署及基本操作详解
    Redis数据库概述 Redis是一个开源的、使用c语言编写NoSQL数据库,它是基于内存运行并支持持久化,采用key-value(键值对)的存储形式, 是目前分布式结构中不可或缺...
    99+
    2024-04-02
  • PythonMySQL数据库基本操作及项目示例详解
    目录一、数据库基础用法二、项目:银行管理系统1、进行初始化操作2、登录检查,并选择操作3、加入查询功能4、加入取钱功能5、加入存钱功能一、数据库基础用法 要先配置环境变量,然后cmd...
    99+
    2024-04-02
  • Swagger及knife4j的基本使用详解
    目录Swagger以及knife4j基本使用Swagger 介绍:Restful 面向资源SpringBoot使用swaggerKnife4j --Swagger增强工具Swagge...
    99+
    2024-04-02
  • React使用redux基础操作详解
    目录一,什么是redux二,安装redux谷歌调试工具三,操作store 改变四,写redux的小技巧一,什么是redux Redux是一个用来管理管理数据状态和UI状态的JavaS...
    99+
    2023-01-13
    React使用redux React redux
  • Qt QTableWidget基本操作及使用是怎样的
    这篇文章将为大家详细讲解有关Qt QTableWidget基本操作及使用是怎样的,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。QTableWidget 是 Qt 中的表格组件类。在...
    99+
    2023-06-21
  • MongoDB Shell常用基本操作命令详解
    目录MongoDB Shell连接数据库库(database)的操作查看所有数据库test 库查看当前数据库删除数据库集合的基本操作创建集合查看集合删除集合其他 Shell 命令清屏...
    99+
    2022-12-08
    MongoDB Shell操作命令 MongoDB Shell
  • MongoDB Shell常用基本操作命令详解
    目录MongoDB Shell连接数据库库(database)的操作查看所有数据库test 库查看当前数据库删除数据库集合的基本操作创建集合查看集合删除集合其他 Shell 命令清屏退出 shell小结MongoDB S...
    99+
    2022-12-05
    MongoDB Shell操作命令 MongoDB Shell
  • Python之字符串的基本操作(很详细)
    一、字符串的索引与切片 Python语言中的字符串包括两种序号体系:正向递增序号和反向递减序号。 1.1 字符串的索引访问 1.2 字符串的切片访问  具体语法格式为:【头下标:尾下标】 ,这种访问方式称之为“切片”。但注意这是左闭右开的...
    99+
    2023-08-31
    python 开发语言
  • SVN安装及基本操作
    SVN(Subversion)是一种版本控制系统,用于管理和追踪文件和目录的变化。下面是SVN的安装及基本操作步骤:**安装SVN*...
    99+
    2023-08-14
    SVN
  • C++中Stack(栈)的使用方法与基本操作详解
    目录一、stack概述二、stack的基本操作1、头文件2、stack创建方式3、栈顶和栈底操作4、元素添加和删除5、栈的大小操作6、判断栈是否为空三、stack的实际应用一、sta...
    99+
    2023-05-19
    C++ Stack栈用法 C++ Stack C++ 栈
  • 详解JavaScript中常用操作符的使用
    目录1、可选链操作符(optional chaining operator)2、空值合并操作符(nullish coalescing operator)3、箭头函数(Arrow Fu...
    99+
    2023-05-17
    JavaScript操作符使用 JavaScript操作符
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作