zl程序教程

您现在的位置是:首页 >  后端

当前栏目

泛函编程(37)-泛函Stream IO:通用的IO处理过程-Free Process详解编程语言

编程IO编程语言 详解 通用 Process stream free
2023-06-13 09:20:38 时间

  在上两篇讨论中我们介绍了IO Process:Process[I,O],它的工作原理、函数组合等。很容易想象,一个完整的IO程序是由 数据源+处理过程+数据终点: Source- Process- Sink所组成的。我们发现:Process[I,O]本身是无法兼顾Source和Sink的功能。而独立附加的Source和Sink又无法有效地与Process[I,O]进行函数组合(functional composition)。

  实际上Process[I,O]是一种固定单一输入类型(single input process):单一是指Process[I,O]只接收I类型输入、固定是指它对外界沟通方式是固定的:只有Halt,Emit,Await三种状态。这种情况导致了Process[I,O]无法成为有效的IO程序组件以及我们必须尝试开发更概括更通用的IO Process。我们来看个新的IO类型结构:Process[F[_],O]

 1 trait Process[F[_],O]{} 

 2 object Process { 

 3 case class Halt[F[_],O](err: Throwable) extends Process[F,O] //终止,err代表停止原因 

 4 case class Emit[F[_],O](os: Seq[O], ns: Process[F,O]) extends Process[F,O] //发送os,ns=nextState 

 5 case class Await[F[_],A,O]( //等待输入 

 6 rq: F[A], //产生输入的运算。可能是有副作用的 

 7 rf: Either[Throwable,A] = Process[F,O], //对运算F[A]返回值的处理函数 

 8 fb: Process[F,O], //fallback 消耗完输入后转入fb状态 

 9 fl: Process[F,O] //finalizer 清理现场,释放资源 

10 ) extends Process[F,O] 

11 }

这个新的类型代表能产生一连串的O类型元素。我们可以把它当作List[O]来编写组件。重要的是我们现在可以通过运算F[A]来与外界联系,如:读取文件或数据库内容。运算F[A]返回的数据经过rf函数的处理后转入新的状态:正常完成数据输入时运行fb进入新状态、如果输入数据过程中产生异常则运行fl清理战场,释放资源。我们并且改善了Halt,使之返回终止情况信息。这样一来Process从整体上更透明,更安全。更重要的是Process可以拓展了(extensible)。

作为一个安全可用的IO类型,我们先设计一些Process[F,O]的基本组件:

1 case object End extends Exception //正常终止信号 

2 case object Kill extends Exception //手工强行终止信号 

3 //运算p,如果出现异常返回Halt使整个过程可控 

4 def Try[F[_],O](p: Process[F,O]): Process[F,O] = 

5 try p 

6 catch { case e: Throwable = Halt[F,O](e) } //返回异常信息

我们在运算Process时用Try来捕捉异常信息并返回到可控状态Halt(err),因为我们可以从返回状态了解终止情况 - End:正常终止,Kill:强行终止及Throwable:发生异常终止。

现在我们再设计一些方便调用的帮助函数(helper functions):

 1 //如果下一个状态还是emit ,那么就一次性合并发出,效率高些 

 2 // emitAll(os1, Emit(os2,ns)) == Emit(os1 ++ os2, ns) 

 3 def emitAll[F[_],O](outseq: Seq[O], nxtState: Process[F,O] = Halt[F,O](End)): Process[F,O] = 

 4 nxtState match { 

 5 case Emit(os,ns) = Emit(outseq ++ os, ns) //下一个状态是Emit, 合并输出 

 6 case _ = Emit(outseq,nxtState) //其它情况照旧 

 7 } 

 8 //遇到单O值时方便使用 

 9 def emit[F[_],O](o: O, nxs: Process[F,O] = Halt[F,O](End)): Process[F,O] = emitAll[F,O](Seq(o),nxs) 

10 //Await类的应用方便函数 

11 def await[F[_],A,O](req: F[A])(rcfn: Either[Throwable,A] = Process[F,O])( 

12 fallback: Process[F,O] = Halt[F,O](End), 

13 finalizer: Process[F,O] = Halt[F,O](End)) = Await(req,rcfn,fallback,finalizer)

还有更多的帮助函数:

 1 //在进入终止状态时运行f 

 2 def onHalt(f: Throwable = Process[F,O]): Process[F,O] = this match { 

 3 case Halt(e) = Try { f(e) } //当前状态是终止,运算f 

 4 case Emit(os,ns) = emitAll(os,ns.onHalt(f)) //还未到终止,该干什么还干什么。移到下一状态再看 

 5 case Await(rq,rf,fb,fl) = await(rq)(rf andThen (_ onHalt(f)))(fb,fl) //还未到终止,移到下一状态再看 

 6 } 

 7 //维持运行直至终止。但不输出任何数据 

 8 def drain[O2]: Process[F,O2] = this match { 

 9 case Halt(e) = Halt(e) 

10 case Emit(os,ns) = ns.drain 

11 case Await(rq,rf,fb,fl) = Await(rq,rf andThen (_ drain)) 

12 } 

13 //正在执行清理环境,释放资源中,维持运行,不得人工终止 

14 def asFinalizer: Process[F,O] = this match { 

15 case Halt(e) = Halt(e) 

16 case Emit(os,ns) = Emit(os, ns.asFinalizer) 

17 case Await(rq,rf,fb,fl) = await(rq){ 

18 case Left(Kill) = this.asFinalizer //如果人工终止,强行继续运行 

19 case x = rf(x) 

20 }(fb,fl) 

21 } 

22 //在终止时运算p,即使是出现了异常情况 

23 def onComplete(p: = Process[F,O]) = 

24 this.onHalt { 

25 case End = p.asFinalizer //正常终止 

26 case err = p.asFinalizer ++ Halt(err) //出现了异常,先运算p,再把异常信息累积起来 

27 }

在上面我们还调用了++函数,目的是把两个process连接起来。如:p.asFinalizer ++ Halt(err)。下面我们就把这个函数实现了:

 1 //连接两个process, this 终止后跟着运算 p 

 2 def ++(p: Process[F,O]): Process[F,O] = this match { 

 3 case Halt(e) = Try { p } //终止了,现在运算 p 

 4 case Emit(os,ns) = emitAll(os, ns ++ p) //还未终止,延后连接 

 5 case Await(rq,rf,fb,fl) = Await(rq, rf andThen (_ ++ p), fb ++ p, fl ++ p) //还未终止,延后连接 

 6 } 

 7 //++的另一种实现方式。我们直接考虑终止状态 

 8 def append(p: Process[F,O]): Process[F,O] = 

 9 onHalt { 

10 case End = Try { p } //我们只会在正常终止的情况下继续运算p 

11 case err = Halt(err) 

12 }

还有我们熟悉的map,flatmap,repeat:

 1 def map[O2](f: O = O2): Process[F,O2] = this match { 

 2 case Halt(e) = Halt(e) 

 3 case Emit(os,ns) = Try { if (os.isEmpty) ns map f 

 4 else emit(f(os.head), emitAll(os.tail, ns) map f) } 

 5 case Await(rq,rf,fb,fl) = Await(rq, rf andThen (_ map f), fb map f, fl map f) 

 6 } 

 7 def flatMap[O2](f: O = Process[F,O2]): Process[F,O2] = this match { 

 8 case Halt(e) = Halt(e) 

 9 case Emit(os,ns) = Try { if (os.isEmpty) ns flatMap f 

10 else f(os.head) ++ emitAll(os.tail, ns).flatMap(f) } 

11 case Await(rq,rf,fb,fl) = Await(rq,rf andThen(_ flatMap f), fb flatMap f, fl flatMap f) 

12 } 

13 def repeat: Process[F,O] = this ++ this.repeat

注意:我们只针对O值的转变。顺便把保证运算安全的几个帮助函数列出来(上面我们已经尝试使用了Try):

 1 //运算p,如果出现异常返回Halt使整个过程可控 

 2 def Try[F[_],O](p: Process[F,O]): Process[F,O] = 

 3 try p 

 4 catch { case e: Throwable = Halt[F,O](e) } //返回异常信息 

 5 //运算p,出现任何异常都先运算fl然后终止 

 6 def TryOr[F[_],O](p: Process[F,O], fl: Process[F,O]): Process[F,O] = 

 7 try p 

 8 catch {case e: Throwable = fl ++ Halt[F,O](e) } 

 9 //运算p,如果正常终止,运行fb, 如果异常终止则运算fl 

10 def TryAwait[F[_],O](p: Process[F,O])(fb: Process[F,O], fl: Process[F,O]): Process[F,O] = 

11 try p 

12 catch { 

13 case End = fb //正常终止 

14 case e: Throwable = fl ++ Halt(e) 

15 }

既然我们说Process[F[_],O]是个更概括的IO类型,那么Process[I,O]应该是Process[F[_],O]的一个特殊案例。现在最重要的是我们需要塑造这个F把它限制在只能接受I类型输入:

 1 case class Is[I]() { 

 2 sealed trait f[X] {} 

 3 case object get extends f[I] //f[X]只有一个实例,就是f[I],所以 X就只能是I 

 4 } 

 5 def Get[I] = Is[I]().get 

 7 type Process1[I,O] = Process[Is[I]#f,O] 

 8 def halt1[I,O]: Process1[I,O] = Halt[Is[I]#f,O](End) 

 9 def emit1[I,O](o: O, ns: Process1[I,O] = halt1[I,O]): Process1[I,O] = emit(o,ns) 

10 def emitAll1[I,O](os: Seq[O], ns: Process1[I,O] = halt1[I,O]): Process1[I,O] = emitAll(os,ns) 

11 def await1[I,O](rf: I = Process1[I,O],fb: Process1[I,O] = halt1[I,O],fl: Process1[I,O] = halt1[I,O]) = 

12 await(Get[I]){(ei: Either[Throwable,I]) = ei match { //F[A] Get[I], 只接收I类型输入 

13 case Left(End) = fb 

14 case Left(err) = halt1[I,O] 

15 case Right(i) = Try { rf(i) } 

16 }}(fb,fl)

 我们同样可以推导出之前针对Process[I,O]的那些组件:

 1 def lift[I,O](f: I = O): Process1[I,O] = 

 2 await1[I,O](i = emit(f(i))) repeat 

 3 def filter[I](f: I = Boolean): Process1[I,I] = 

 4 await1[I,I](i = if (f(i)) emit(i) else halt1) 

 5 def take[I](n: Int): Process1[I,I] = 

 6 if (n 0) await1[I,I](i = emit(i, take(n-1))) 

 7 else halt1 

 8 def takeWhile[I](f: I = Boolean): Process1[I,I] = 

 9 await1[I,I](i = if (f(i)) emit(i, takeWhile(f)) else halt1) 

10 //循环入什么就出什么 

11 def id[I]: Process1[I,I] = await1[I,I](i = emit(i,id)) 

12 def passUnchanged[I]: Process1[I,I] = lift[I,I](identity) 

13 def drop[I](n: Int): Process1[I,I] = 

14 if (n 0) await1[I,I](i = drop[I](n-1)) 

15 else passUnchanged 

16 def dropWhile[I](f: I = Boolean): Process1[I,I] = 

17 await1[I,I](i = if (f(i)) id else emit(i, dropWhile(f))) 

注意:组件实现中的写法和之前Process[I,O]的一样,只不过这次我们的返回类型是Process1[I,O]。

现在我们来到了IO Process对接最重要的组件导管(pipe | )组件了:

 1 //强行终止process 

 2 @annotation.tailrec 

 3 final def kill[O2]: Process[F,O2] = this match { 

 4 case Halt(e) = Halt(e) //已经处于终止状态 

 5 case Emit(os,ns) = ns.kill //发送中,忽略输出kill下个状态 

 6 case Await(rq,rf,fb,fl) = rf(Left(Kill)).drain.onHalt { //如果正在等待输入,那么就给它输入Kill信号然后过滤余下的输入 

 7 case Kill = Halt(End) //在终止前如果收到Kill信号就转入正常终止状态 

 8 case err = Halt(err) //如果是异常终止 

 9 } 

10 } 

12 //对接两个process. 上一个emit发送后下面的立即await接受 

13 def | [O2](p2: Process1[O,O2]): Process[F,O2] = { 

14 @annotation.tailrec 

15 //把Seq[O]一个O一个O喂入p2 

16 def feed(os: Seq[O], ns: Process[F,O], rf: Either[Throwable,O] = Process1[O,O2], 

17 fb: Process1[O,O2] = halt1[O,O2], fl: Process1[O,O2] = halt1[O,O2]): Process[F,O2] = 

18 if (os.isEmpty) ns | await(Get[O])(rf)(fb,fl) 

19 else rf(Right(os.head)) match { //喂进去一个元素后再观察下面的状态 

20 case Await(rq1,rf1,fb1,fl1) = feed(os.tail,ns,rf,fb,fl) 

21 case p = Emit(os.tail,ns) | p 

22 } 

23 p2 match { 

24 case Halt(e) = this.kill.onHalt {e2 = Halt(e) ++ Halt(e2)} //如果下游管道读者了。杀掉上游并过滤所有输出 

25 case Emit(os,ns) = emitAll(os, this | ns) 

26 case Await(rq,rf,fb,fl) = this match { 

27 case Halt(e) = Halt(e) | p2 

28 case Await(rq0,rf0,fb0,fl0) = await(rq0)(rf0 andThen (_ | p2))(fb0 | p2, fl0 | p2) 

29 case Emit(os,ns) = Try {feed(os,ns,rf,fb,fl)} //上游发送下游接受状态。正是对接状态 

30 } 

31 } 

32 }

我们只需要考虑下游p等待输入await同时上游this正在发送emit这一个状态实现对接。其它状态则等它们自己调整对口后再对接。

有了这个| 后我们可以把那些Process1组件对接到Process[F,O]上:

1 def filter(f: O = Boolean): Process[F,O] = this | Process.filter(f) 

2 def take(n: Int): Process[F,O] = this | Process.take(n) 

3 def takeWhile(f: O = Boolean): Process[F,O] = this | Process.takeWhile(f) 

4 def drop(n: Int): Process[F,O] = this | Process.drop(n) 

5 def dropWhile(f: O = Boolean): Process[F,O] = this | Process.dropWhile(f)

以上组件都是过滤输出O的。

我们还可以通过Process[F,O]实现两头输入:就像字母T,输入从上面左右两头进入。

首先,我们还是要重新塑造一下F[A],使它只容许左边I类,右边I2类输入:

1 case class T[I,I2]() { 

2 sealed trait f[X] {def get: Either[I = X, I2 = X]} 

3 val L = new f[I] { def get = Left(identity) } 

4 val R = new f[I2] { def get = Right(identity) } 

6 def L[I,I2] = T[I,I2]().L 

7 def R[I,I2] = T[I,I2]().R 

8 type Tee[I,I2,O] = Process[T[I,I2]#f,O]

Tee类型就是一个两头输入的IO Process类型,左边只可以输入I,右边只可以输入I2。

我们同样需要重新定义那些构建Tee的基本组件:

 1 def haltT[I,I2,O] = Halt[T[I,I2]#f,O](End) 

 2 def emitT[I,I2,O](o: O, ns: Tee[I,I2,O] = haltT) = emit[T[I,I2]#f,O](o,ns) 

 3 //左边输入 

 4 def awaitL[I,I2,O](rf: I = Tee[I,I2,O], fb: Tee[I,I2,O] = haltT, fl: Tee[I,I2,O] = haltT): Tee[I,I2,O] = 

 5 await[T[I,I2]#f,I,O](L){ //F[A] L 只容许I类输入 

 6 case Left(End) = fb 

 7 case Left(err) = fl ++ Halt(err) 

 8 case Right(i) = Try { rf(i) } 

 9 }(fb,fl) 

10 //右边输入 

11 def awaitR[I,I2,O](rf: I2 = Tee[I,I2,O], fb: Tee[I,I2,O] = haltT, fl: Tee[I,I2,O] = haltT): Tee[I,I2,O] = 

12 await[T[I,I2]#f,I2,O](R) { //F[A] R 只容许I2类输入 

13 case Left(End) = fb 

14 case Left(err) = fl ++ Halt(err) 

15 case Right(i2) = Try { rf(i2) } 

16 }(fb,fl)

zip是两边输入穿插动作。我们可以用这个Tee类型来实现zip:

 1 def zipWith[I,I2,O](f: (I,I2) = O): Tee[I,I2,O] = 

 2 awaitL[I,I2,O](i = awaitR[I,I2,O](i2 = emitT(f(i,i2)))) repeat 

 3 //两个输入交叉输出一个对值pair 

 4 def zip[I,I2]: Tee[I,I2,(I,I2)] = zipWith[I,I2,(I,I2)]((_,_)) 

 5 //轮换从左右两边输入 

 6 def interleave[I]: Tee[I,I,I] = awaitL[I,I,I](i = awaitR(i2 = emitT(i) ++ emitT(i2))) repeat 

 7 //不理会右边输入,只取左边 

 8 def passL[I,I2]: Tee[I,I2,I] = awaitL(emitT(_,passL)) 

 9 //不理会左边输入,只取右边 

10 def passR[I,I2]: Tee[I,I2,I2] = awaitR(emitT(_,passR)) 

11 //完成左边输入后运算fb 

12 def awaitLOr[I,I2,O](fb: Tee[I,I2,O])(rf: I = Tee[I,I2,O]): Tee[I,I2,O] = 

13 awaitL(rf,fb) 

14 //完成右边输入后运算fb 

15 def awaitROr[I,I2,O](fb: Tee[I,I2,O])(rf: I2 = Tee[I,I2,O]): Tee[I,I2,O] = 

16 awaitR(rf,fb) 

17 //如果两边输入长度不同就在左边补填padI或右边补填padI2 

18 def zipWithAll[I,I2,O](padI: I, padI2: I2)(f: (I,I2) = O): Tee[I,I2,O] = { 

19 val fbL = passL[I,I2] map {f(_, padI2)} //假如先完成左边输入,运算fbL,返回补填padI2的Tee类型 

20 val fbR = passR[I,I2] map {f(padI, _)} //假如先完成右边输入,运算fbR,返回补填padI的Tee类型 

21 awaitLOr(fbL)(i = 

22 awaitROr(fbR)(i2 = emitT(f(i,i2)))) repeat 

23 }

下面是Tee的一个通用概括的帮助函数,它是两头输入的代表形式:

 1 //接入左右两边输入 

 2 def tee[O2,O3](p2: Process[F,O2])(t: Tee[O,O2,O3]): Process[F,O3] = { 

 3 @annotation.tailrec 

 4 def feedL(os: Seq[O], ns: Process[F,O], p: Process[F,O2], t: Tee[O,O2,O3], //从左边喂入 

 5 rf: Either[Throwable,O] = Tee[O,O2,O3]): Process[F,O3] = //输入类型O 

 6 if (os.isEmpty) (ns tee p)(t) //喂完所有输入元素 

 7 else rf(Right(os.head)) match { //喂进一个O后检查状态 

 8 case Await(rq0,rf0,fb0,fl0) = feedL(os.tail,ns,p,t,rf0) //下一个状态还是await的话继续喂os.tail 

 9 case t0 = (Emit(os.tail,ns) tee p)(t0) //tee转入新状态 

10 } 

11 @annotation.tailrec 

12 def feedR(os: Seq[O2], ns: Process[F,O2], p: Process[F,O], t: Tee[O,O2,O3], //从右边喂入 

13 rf: Either[Throwable,O2] = Tee[O,O2,O3]): Process[F,O3] = //输入类型O2 

14 if (os.isEmpty) (p tee ns)(t) //喂完所有输入元素 

15 else rf(Right(os.head)) match { //喂进一个O2后检查状态 

16 case Await(rq0,rf0,fb0,fl0) = feedR(os.tail,ns,p,t,rf0) //下一个状态还是await的话继续喂os.tail 

17 case t0 = (p tee Emit(os.tail,ns))(t0) //tee转入新状态 

18 } 

19 t match { 

20 case Halt(e) = this.kill onComplete p2.kill onComplete Halt(e) //T管道停止就杀掉左右输入process 

21 case Emit(os,ns) = emitAll(os, (this tee p2)(ns)) //T正在发送,等发送完再tee 

22 case Await(side,rf,fb,fl) = side.get match { //T管道正在等待输入,先确定是左右那边输入 

23 case Left(isO) = this match { //左边输入,类型O 

24 case Halt(e) = p2.kill onComplete Halt(e) //左边输入终止,杀掉右边输入保持两边同步 

25 case Await(rqL,rfL,fbL,flL) = await(rqL)(rfL andThen (this2 = this2.tee(p2)(t)))() //左边也在等输入 

26 case Emit(os,ns) = Try {feedL(os,ns,p2,t,rf)} //左边在发送,用feedL逐个O喂入 

27 } 

28 case Right(isO2) = p2 match { 

29 case Halt(e) = this.kill onComplete Halt(e) //右边输入终止,杀掉左边输入保持两边同步 

30 case Await(rqR,rfR,fbR,flR) = await(rqR)(rfR andThen (p = this.tee(p)(t)))() 

31 case Emit(os,ns) = Try {feedR(os,ns,this,t,rf)} //右边在发送,用feedL逐个O喂入 

32 } 

34 } 

35 } 

36 }

现在zipWith可以这样写了:

1 //用tee来实现zipWith 

2 def zipWith[O2,O3](p2: Process[F,O2])(f: (O,O2) = O3): Process[F,O3] = 

3 (this tee p2)(Process.zipWith(f))

一个完整的IO程序必须包括对数据源Source和数据终点Sink的操作,那么Process[F,O]可不可以代表数据源(Source)类型呢?我们来看看Process[F,O]的读取Await: 

1 case class Await[F[_],A,O]( //等待输入 

2 rq: F[A], //产生输入的运算。可能是有副作用的 

3 rf: Either[Throwable,A] = Process[F,O], //对运算F[A]返回值的处理函数 

4 fb: Process[F,O] = Halt[F,O](End), //fallback 消耗完输入后转入fb状态 

5 fl: Process[F,O] = Halt[F,O](End) //finalizer 清理现场,放开所有使用中的资源 

6 ) extends Process[F,O]

如果我们把F[A]换成IO[A]:

1 case class Await[IO[_],A,O]( //等待输入 

2 rq: IO[A], //产生输入的运算。可能是有副作用的 

3 rf: Either[Throwable,A] = Process[IO,O], //对运算F[A]返回值的处理函数 

4 fb: Process[IO,O] = Halt[IO,O](End), //fallback 消耗完输入后转入fb状态 

5 fl: Process[IO,O] = Halt[IO,O](End) //finalizer 清理现场,放开所有使用中的资源 

6 ) extends Process[IO,O]

实际上Process[I,O]就是Process[F,O]的一个案例。我们只要运算IO就可以从数据源读取数据了(run IO 返回结果)。我们先看看IO类型,我们前面曾经讨论过这个类型:

 1 //简化的IO类,只能用做示范 

 2 trait IO[A] { self = 

 3 def run: A 

 4 def map[B](f: A = B): IO[B] = 

 5 new IO[B] { def run = f(self.run) } 

 6 def flatMap[B](f: A = IO[B]): IO[B] = 

 7 new IO[B] { def run = f(self.run).run } 

 8 } 

 9 object IO { 

10 def unit[A](a: = A): IO[A] = new IO[A] { def run = a } 

11 def flatMap[A,B](fa: IO[A])(f: A = IO[B]) = fa flatMap f 

12 def apply[A](a: = A): IO[A] = unit(a) // syntax for IO { .. } 

13 }

现在我们可以写个从数据源Source读取数据的程序了:

 1 //从数据源src把内容读入内存 

 2 def collect[O](src: Process[IO,O]): IndexedSeq[O] = { //从src移到IndexedSeq 

 3 def go(curState: Process[IO,O], accSeq: IndexedSeq[O]): IndexedSeq[O] = 

 4 curState match { 

 5 case Halt(e) = accSeq //完成,返回累积数据 

 6 case Emit(os,ns) = go(ns, accSeq ++ os) //当前处于发送数据状态,把发送数据累积到accSeq 

 7 case Await(rq,rf,fb,fl) = go(Try { rf(Right(rq.run)) }, accSeq) //正在读取数据中 

 8 //如果读取发生异常Try函数会返回终止Halt(err) 

 9 } 

10 go(src,IndexedSeq()) 

11 }

注意以上只是示范了Process[IO,O]作为Process[F,O]的一个特殊实例是可以实现从Source读取数据的。我们将在下个章节讨论具体的可行方案。

我们下面再看看数据终点Sink与Process[F,O]的关系。我们希望通过Process[F,O]类型实现输出功能,也就是把Source的输入发送给Sink。我们用以下方式代表Sink:

 type Sink[F[_],O] = Process[F, O = F[Unit]] 

这个应该不难理解:Sink为输出O提供了一系列函数。这些函数接收输入参数O然后运行F,F就是个运算不返回结果,如IO运算。

Source和Sink类型的实际应用介绍将在下期“IO过程实际应用-IO Process in action”中具体讨论。

 

 

 

 

 

 

 

 

 

 

 

原创文章,作者:Maggie-Hunter,如若转载,请注明出处:https://blog.ytso.com/12948.html

cgojava