Scala语法详解之基本声明与定义_Android, Python及开发编程讨论区_Weblogic技术|Tuxedo技术|中间件技术|Oracle论坛|JAVA论坛|Linux/Unix技术|hadoop论坛_联动北方技术论坛  
网站首页 | 关于我们 | 服务中心 | 经验交流 | 公司荣誉 | 成功案例 | 合作伙伴 | 联系我们 |
联动北方-国内领先的云技术服务提供商
»  游客             当前位置:  论坛首页 »  自由讨论区 »  Android, Python及开发编程讨论区 »
总帖数
1
每页帖数
101/1页1
返回列表
0
发起投票  发起投票 发新帖子
查看: 3335 | 回复: 0   主题: Scala语法详解之基本声明与定义        下一篇 
wei.wang
注册用户
等级:少校
经验:1001
发帖:87
精华:0
注册:2013-8-29
状态:离线
发送短消息息给wei.wang 加好友    发送短消息息给wei.wang 发消息
发表于: IP:您无权察看 2013-9-5 15:08:29 | [全部帖] [楼主帖] 楼主

4. 基本声明与定义

语法:

Dcl
::= „val‟ ValDcl
| „var‟ VarDcl
| „def‟ FunDcl
| „type‟ {nl} TypeDcl
PatVarDef
Def
::= „val‟ PatDef
| „var‟ VarDef
::= PatVarDef
| „def‟ FunDef
| „type‟ {nl} TypeDef
| TmplDef


声明引入命名并给其指定类型。声明可以是类定义(§5.1)或者复合类型(§3.2.7)中

修饰定义的一部分。

定义引入命名用以表示术语或类型。定义可以是对象或类定义的一部分,或者只是局

限在一个代码块中。声明和定义都会产生关联类型命名和类型定义与边界的绑定,将术语

名称和类型联系起来。

声明或定义引入的命名的范围是包括该绑定的整个语句序列。。然而在代码块的前向引

用中有一个限制:语句序列 s1...sn 构成一个代码块,如果 si 中的一个简单命名引用一

个定义在 sj 中的实体,且 j>=i,那么 si 和 sj 之间(包括这两者)的定义不能是值或者变

量定义。

4.1.

语法:

Dcl


值声明与定义

::= „val‟ ValDcl
::= ids „:‟ Type
::= „val‟ PatDef
::= Pattern2 {„,‟ Pattern2} [„:‟ Type] „=‟ Expr
::= id {„,‟ id}
ValDcl
Def
PatDef
ids


值声明 val x: T 表示 x 是一个类型为 T 的值的命名。

值定义 val x: T = e 表示 x 是表达式 e 求值的结果。如果值的定义不是递归的,

类型 T 可以忽略,默认就是表达式 e 的打包类型(§6.1)。如果给出了类型 T,那么 e 就

被期望与其一致。

28


值定义的求值就是其右侧表达式 e 的求值,除非有一个 lazy 修饰符。值定义的效果

是将 x 绑定到变为类型 T 的 e 的结果。lazy 型的值定义只在值第一次被访问时才对右侧

表达式求值。

值定义可以在左侧有一个模式(§8.1)。如果 p 是除了简单命名或命名后跟冒号与类

型的模式,那么值定义 val p = e 可扩展为以下形式:

1. 如果模式 p 具有绑定变量 x1,...,xn,n>1:

val $x = e match {case p => {x1,...,xn}}
val x1 = $x._1
...
val xn = $x._n


   这里的$x 是一个新命名。

2. 如果 p 有一个唯一的绑定变量 x:

val x = e match { case p => x }


3. 如果 p 没有绑定变量:

e match { case p => ()}


示例 4.1.1 以下是一些值定义的例子:

val pi = 3.1414
val pi: Double = 3.1415
val Some(x) = f()
val x :: xs = mylist
//与第一个等价
//模式定义
//中缀模式定义


后两个定义具有以下扩展:

val x = f() match { case Some(x) = x }
val x$ = mylist match { case x :: xs => {x, xs} }
val x = x$._1
val xs = x$._2


   声明或定义的值的命名不能以_=结束。

     值声明 val x1,...,xn: T 是 val x1: T; ...; val xn: T 的简写形式。值定

义 val p1,...,pn = e 是 val p1 = e; ...; val pn = e 的简写形式。值定义

val p1,...,pn: T = e 是 val p1:T = e; ...; val pn:T = e;的简写形式。

4.2.

语法:

Dcl
Def


变量声明与定义

::= „var‟ VarDcl
::= „var‟ VarDef
::= ids „:‟ Type
::= PatDef
| ids „:‟ Type „=‟ „_‟
VarDcl
VarDef


变量声明 var x: T 等价于声明一个 getter 函数 x 和一个 setter 函数 x_=,如

下所示:

def x: T
29
def x_= (y: T): Unit


   包括变量声明的类的实现可以用变量定义来定义这些变量,也可以直接定义 getter

和 setter 函数。

     变量定义 var x: T = e 引入了一个类型为 T 的可变变量,并用表达式 e 作为初始

值。类型 T 可忽略,默认用 e 的类型。如果给定了 T,那么 e 被期望具有一致的类型

(§6.1)。


   变量定义可以在左侧有一个模式(§8.1)。如果 p 是除了简单命名或命名后跟冒号与

类型的模式,那么变量定义 var p = e 具有和值定义一致的扩展模式,除了那些 p 中自

由的命名是可变的变量,不是值。

     任何声明或定义的变量的命名不能以_=结尾。

     变量定义 var x: T = _只能以模板成员出现。该定义表示一个可变字段和一个默认

初始值。该默认值取决于类型 T:

     0如果 T 是 Int 或其子类型

     0L如果 T 是 Long

     0.0f如果 T 是 Float

     0.0d如果 T 是 Double

     false 如果 T 是 Boolean

     {}如果 T 是 Unit

     null所有其他类型

     如果变量定义是模板成员,那么他们同时引入一个 getter 函数 x(返回当前赋给变量

的值)和一个 setter 函数 x_=(改变当前赋给变量的值)。函数具有与变量声明相同的识

别标识。模板具有 getter 和 setter 函数成员,初始的变量不能以模板成员的形式被直

接访问。

示 例 4.2.1 下 面 的 例 子 显 示 了 Scala 中 如 何 模 拟 属 性 。 首 先 定 义 了 一 个 类

TimeOfDayVar,具有可更新的整型字段表示时分秒。其实现包括一些测试,只允许合法

值赋给这些字段。但是用户代码可以像访问其他变量那样直接访问这些字段。

class TimeOfDayVar {
      private var h: Int = 0
      private var m: Int = 0
      private var s: Int = 0
      def hours
      def hours_= (h: Int)
      def minutes
      def minutes_= (m: Int)
      def seconds
      def seconds_= (s: Int)
}
val d = new TimeOfDayVar
d.hours = 8; d.minutes = 30; d.seconds = 0
30
=h
= if (0 <= h && h < 24) this.h = h
else throw new DataError()
=m
= if(0<= m && m < 60) this.m = m
else throw new DataError()
=s
= if (0 <= s && s < 60) this.s = s
else throw new DataError()
//抛出一个 DataError 异常。
d.hours = 24


变量声明 var x1,...,xn: T 是 var x1: T; ...; var xn: T 的简写形式。变

量定义 var x1,...,xn = e 是 var x1 = e; ...; var xn = e 的简写形式。变量

定义 var x1,...,xn: T = e 是 var x1:T = e; ...; var xn:T = e 的简写形式。

4.3.

语法:

Dcl
Def


类型声明与类型别名

::= „type‟ {nl} TypeDcl
::= type {nl} TypeDef
TypeDcl ::= id [TypeParamClause] [„>:‟ Type] [„<:‟ Type]
TypeDef ::= id [TypeParamClause] „=‟ Type


   类型声明 type t[tps] >: L <: U 声明了 t 是一个下界为 L 上界为 U 抽象类型。

如果类型参数子句[type]被忽略的话,t 是一个一阶类型的抽象,否则,t 就是一个类型

构造器,接受类型参数子句中指明的参数。

     如 果 一 个 类 型 声 明 是 一个 类 型 的 成 员 声 明 , 该类 型 的 实 现 可 以 用 任 何符 合 条 件

L<:T<:U 的类型 T 来实现 t。如果 L 与 U 不一致将会导致编译时错误。边界的某边或全

部可被忽略。如果没有给出下界 L,类型 scala.Nothing 就会是默认的下界。如果没有

给出上界 U,类型 scala.Any 就会是默认的上界。

     类型构造器声明给与 t 有关的具体类型加上了额外的限制。除了边界 L 和 U,类型参

数子句可以引入由类型构造器一致性(§3.5.2)控制的高阶边界和差异。

     类型参数的域超越了边界 >:L <:U 和类型参数子句 tps 本身。(抽象类型构造器 tc

的)高阶类型参数子句拥有由类型参数声明 tc 限制的同样的域。

     一些与嵌套域有关的例子,以下声明是完全等价的:type t[m[x] <: Bound[x],

Bound[x]] , type t[m[x] <: Bound[x], Bound[Y]] 和 type t[m[x] <:

Bound[x], Bound[_]],类型参数 m 的域限制为 m 的声明。以上所有情况中,t 是在两

个类型构造器上抽象的抽象类型成员:m 是有一个类型参数并且是 Bound 的子类型的类型

构造器,t 是第二个类型构造参数。t[MutableList, Iterable]是 t 的合法用法。

     类型别名 type t = T 定义了 t 是类型 T 的别名命名。类型别名的左侧可以是一个

类型参数子句,比如 type t[tps] = T。类型参数的域超过了右侧的 T 和类型参数子句

本身。

     定义(§4)与类型参数(§4.6)的域的规则使类型命名在其自身或右侧出现成为可能。

然而如果类型别名递归引用到定义的类型构造器自身将会导致静态错误。也就是类型别名

type t[tps] = T 中的类型 T 不能直接或间接的引用到命名 t。如果抽象类型是其自身

直接或间接的上界或下界也会导致错误。

示例 4.3.1 下面是合法的类型声明与定义:

type IntList = List[Integer]
type T <: Comparable[T]
type Two[A] = Tuple2[A, A]
type MyCollection[+X] <: Iterable[X]


以下是非法的:

31
type Abs = Comparable[Abs]
type S <: T
type T <: S
//递归类型别名
//S, T 自我绑定
type T >: Comparable[T.That]
//无法从 T 选择
//T 是类型而不是值
type MyCollection <: Iterable //类型构造器必须显式列出参数


如果类型别名 type t[tps] = S 指向类类型 S,命名 t 也可用作类型为 S 的对象的

构造器。

示例 4.3.2 下面的 Predef 对象包括了一个定义,将 Pair 作为参数化类 Tuple2 的别

名:

type Pair[+A, +B] = Tuple2[A, B]


   因此,对于任意的两个类型 S 和 T,类型 Pair[S, T]等价于类型 Tuple2[S, T]。

Pair 还可以用来作为 Tuple2 构造器的替换。并且由于 Tuple2 是一个 case 类,Pair

也是 case 类工厂 Tuple2 的别名,这在表达式或模式中均有效。因此以下都是 Pair 的

合法使用。

val x: Pair[Int, String] = new Pair(1, “abc”)
val y: Pair[String, Int] = x match {
      case Pair(i, s) => Pair(z + i, i * i)
}
4.4.


语法:

类型参数

::= „[‟ VariantTypeParam {„,‟ VariantTypeParam} „]‟
::= [„+‟ | „-‟] TypeParam
::= (id | „_‟) [TypeParamClause] [„>:‟ Type]
[„<:‟ Type] [„>%‟ Type]
TypeParamClause
VariantTypeParam
TypeParam


  类型参数在类型定义,类定义和函数定义中出现。本节中我们只考虑有下界>:L 和上

界<:U 的类型参数定义,视图边界<%U 将在 7.4 节中讨论。

     一阶类型参数最一般的形式是+/- t >:L <:U。这里 L 和 U 是下界和上界,限制了

参数的可能的类型参量。如果 L 与 U 不一致将会导致编译错误。+/-是差异,指一个可选

前缀+或-。

     在类型参数子句中的所有类型参数的名称必须两两不同。类型参数的作用域在每个类

型参数子句中。因此类型参数作为自己边界的一部分或同一子句中其他类型参数的边界出

现是合理的。然而,类型参数不能直接或间接的作为自己的边界。

     类型构造参数给类型参数增加了一个嵌套的类型参数子句。最常见的类型构造器参数

的形式是+/- t[tps] >: L <: U.

     以上域的限制可归纳为嵌套类型参数子句,该子句声明了高阶的类型参数。高阶类型

参数(类型参数 t 的类型参数)只在他们直接包围的参数子句(可能包括更深嵌套层次的子

句)和 t 的边界中可见。因此他们的名字只需与其他可见参数两两不同。由于高阶类型参

32


数的命名往往是无关的,因此可以用‟_‟来指示它们,这种情况下其他地方均不可见。

示例 4.4.1 下面是一些合法的类型参数子句:

[S, T]
[Ex <: Throwable]
[A <: Comparable[B], B <: A]
[A, B >: A, C >: A <: B]
[M[X], N[X]]
[M[_], N[_]]
//和上一个等价
[M[X <: Bound[X]], Bound[_]]
[M[+X] <: Iterable[X]]


以下是一些非法的类型参数子句:

[A >: A]
[A <: B, B <: C, C <: A]
[A, B, C >: A <: B]
//非法,“A”做了自己的边界
//非法,“A”做了自己的边界
//非法,“C”的下界“A”与上界“B”不一致
4.5.


差异标注

差异标注指示了参数化类型的实例在子类型(§3.5.2)上是如何不同的。“+”类型的

差异指协变的依赖,“-”类型的差异指逆变的依赖,未标注指不变依赖。

差异标注限制了被标注类型变量在与类型参数绑定的类型或类中出现的方式。在类型

定义 type T[tps] = S 或类型声明 type T[tps] >: L <: U 中,“+”标注的类型

参数只能出现在协变的位置,“-”标注的类型参数只能出现在逆变的位置。类似地,对于

类定义 class C[tps](ps) exntends T x: S => ...@, “+”标记的类型参数只

能出现在类型自身 S 和模板 T 的协变位置,“-”标记的类型参数只能出现在逆变位置。

类型或模板中类型参数的协变位置定义如下:与协变相反的是逆变,非变与其自身相

反。最高级的类型或模板总是在协变位置。差异位置按照下述方式变化:

l 方法参数的差异位置是参数子句差异位置的相对位置。

l 类型参数的差异位置是类型参数子句差异位置的相对位置

l 类型声明或类型参数的下界的差异位置是类型声明或参数差异位置的相对位置

l 类型别名 type T[tps] = S 右侧的 S 总是处于非变位置。

l 可变量的类型总是处于非变位置

l 类型选择 S#T 的前缀 S 总是处于非变位置

l 类型 S[…T…]的类型参量 T:如果对应的类型参数是非变的,那么 T 就在非变位

     置。如果对应的类型参数是逆变的,那么 T 的差异位置就是类型 S[…T…]的差异

     位置的相对位置。

到类的对象私有的值,变量或方法的引用的差异并未被检查。这些成员中类型参数可

以出现在任意位置,并未限制其合法的差异标注。

示例 4.5.1 下面的差异标注是合法的:

abstract class P[+A, +B] {
      def fst: A; def snd: B
}


有了这个差异标注,类型 P 的子类型将对其参量自动协变。例如,

33
P[IOException, String] <: P[Throwable, AnyRef]


如果我们使 P 的元素可变,差异标注就非法了。

abstract class Q[+A, +B](x: A, y: B) {
      var fst: A = x
      var snd: B = y
}
//*** 错误:非法差异:
//„A‟, „B‟出现在非变位置。


如果可变变量是对象私有的,那么类定义就是合法的了:

abstract class R[+A, +B](x: A, y: B){
      private[this] var fst: A = x
      private[this] var snd: B = y
}
//OK
//OK


示例 4.5.2 下面的差异标注是非法的,因为 a 出现在 append 的参数的逆变位置:

abstract class Vector[+A] {
      def append(x: Vector[A]): Vector[A]
      //**** 错误:非法的差异:
      //‟A‟出现在逆变位置
}


这个问题可以通过对下界求值的方式将 append 的类型泛化来解决。

abstract class Vector[+A] {
      def append[B >: A](x: Vector[B]): Vector[B]
}


示例 4.5.3 下面是逆变类型参数有用的一个例子。

abstract class OutputChannel[-A] {
      def write(x: A): Unit
}


有了这个标注,OutputChannel[AnyRef]将和 OutputChannel[String]一致。

也就是一个可以写任何对象的 channel 可以代替只能写 String 的 channel

4.6.

Dcl


函数声明与定义

::= „def‟ FunDcl
::= FunSig : Type
::= „def‟ FunDef
::= FunSig [„:‟ Type] „=‟ Expr
::= id [FunTypeParamClause] ParamClauses
::= {ParamClauses} [[nl] „(‟ „implicit‟ Params „)‟]
::= [nl] „(‟ [Params] „)‟}


语法:

FunDcl
Def
FunDef
FunSig
ParamClauses
ParamClause
34
FunTypeParamClause ::= „[‟ TypeParam {„,‟ TypeParam} „]‟
Params
Param
ParamType
::= Param {„,‟ Param}
::= {Annotation} id [„:‟ ParamType]
::= Type
| „=>‟ Type
| Type „*‟


   函数声明具有这样的形式:def f psig: T,f 是函数的名称,psig 是参数签名,

T 是返回类型。函数定义 def f psig: T = e 还包括了函数体 e,例如一个表达式定义

了函数的结果。参数签名由一个可选的类型参数子句[tps],后跟零个或多个值参数子句

(ps1)…(psn)构成。这样的声明或定义引入了一个值,该值具有一个(可能是多态的)方法

类型,其参数类型与返回类型已给出。

     已给出的函数体的类型被期望与函数声明的返回类型一致(§6.1)。如果函数定义不

是递归的,那么返回类型则可省略,因为其可由函数体打包的类型推断出来。

     类型参数子句 tps 由一个或多个类型声明(§4.3)构成,在其中引入了可能具有边界

的类型参数。类型参数的域包括整个签名,也包括任何类型参数边界以及函数体(如果有

的话)。

     值参数子句 ps 由零个或多个规范类型绑定(如 x: T)构成,这些类型绑定绑定了值参

数以及将它们与它们的类型联系起来。一个规范值参数命名 x 的范围是函数体(如果有的

话)。所有的类型参数名及值参数名必须两两不同。

4.6.1. 叫名参数

语法:

ParamType
::= „=>‟ Type


  值参数类型可以有前缀=>,例如: x: => T。这样一个参数的类型就是无参方法类

型=>T。这表明对应的参数并没有在函数应用处求值,而是在函数中每次使用时才求值。

也就是该参数以叫名的方式求值。

示例 4.6.1 声明:

def whileLoop (cond: => Boolean) (start: => Unit): Unit


表示 whileLoop 的所有参数都以叫名的方式求值。

4.6.2. 重复参数

语法:

ParamType
::= Type „*‟


 参数段中的最后一个参数可以有后缀„*‟,例如:(…, x: T*)。方法中这样一个重

复参数的类型就是序列类型 scala.Seq[T]。具有重复参数 T*的方法具有可变数目的类

型为 T 的参数。也就是,如果方法 m 的类型(T1,…,Tn,S*)U 应用到参数(e1,…,ek)上,

且有 k>=n,那么 m 就被认为在应用中具有类型(T1,…,Tn,S,…,S)U,S 重复 k-n 次。这

个规则的唯一例外是如果最后一个参数用_*类型标注的方式被标记为一个序列参量。如果

以 上 的 m 应 用 到 参 数 (e1,…,en,e‟:_*) 上 , 那 么 该 应 用 中 m 的 类 型 就 被 认 为

35
(T1,…,Tn,scala.Seq[S])。


示例 4.6.2 以下方法定义计算了可变数目的整形参数的和:

def sum(args: Int*) = {
      var result = 0
      for(arg <- args.elements) result += arg
      result
}


以下对该方法的应用可得出的结果为 0,1,6:

sum()
sum(1)
sum(1,2,3)


更进一步的,假设以下定义:

var xs = List(1,2,3)


以下对方法 sum 的应用是错误的:

sum(xs)
// ***** error: expected: Int, found: List[Int]


相比较,以下应用是正确的,并产生结果 6:

sum(xs:_*)


4.6.3. 过程

语法:

FunDcl
FunDef
::= FunSig
::= FunSig[nl] „{‟Block„}‟


过程有特殊语法,例如,返回 Unit 值{}的函数。过程声明只是返回类型被忽略的函

数声明。返回类型自动定义为 Unit 类型。例如 def f(ps)等价于 def f(ps):Unit。

过程定义是返回类型及等号被忽略的函数定义;其定义表达式必须是一个代码块。例

如:def f(ps){stats}等价于 def f(ps):Unit={stats}.

示例 4.6.3 以下是名为 write 的过程的声明与定义:

trait Writer {
      def write(str: String)
}
object Terminal extends Writer{
def write(str: String) {System.out.println(str)}
}


以上代码被内部自动完成为:

trait Writer {
      def write(str: String): Unit
      36
}
object Terminal extends Writer{
def write(str: String): Unit = {System.out.println(str)}
}


4.6.4. 方法返回类型推断

类成员定义 m 重载了基类 C 中的一些其他的函数 m‟可以略去返回类型,即使是递归

的也无所谓。因此被重载的函数 m‟的返回类型 R‟(被认为是 C 的成员) 在对 m 的每次调

用中被认为是 m 的返回类型。在以上方式中,m 右侧的类型 R 可以被确定,并作为 m 的返

回类型。注意到 R 可以与 R‟不同,只要 R 与 R‟一致即可。

示例 4.6.4 假定有以下定义:

trait I {
      def factorial(x: Int): Int
}
class C extends I {
      def factorial(x: Int) = if (x==0) 1 else x * factorial(x - 1)
}


这里忽略 C 中 factorial 的返回类型是没问题的,即使这是一个递归的方法。

4.7.

语法:

Import 子句

::= „import‟ ImportExpr {„,‟ ImportExpr}
::= StableId „.‟ (id | „_‟ | ImportSelectors)
Import
ImportExpr
ImportSelectors ::= „{‟ { ImportSelector „,‟} (ImportSelector | „_‟) „}‟
ImportSelector ::= id [„=>‟ id | „=>‟ „_‟]


import 子句形式为 import p.I,p 是一个稳定标识符(§3.1),I 是一个 import

表达式。import 表达式确定了 p 的成员中一些名称的集合,使这些名称不加限定即可用。

最普通的 import 表达式的形式是一个 import 选择器的列表。

{x1=>y1,…,xn=>yn,_}


   其中 n>=0,最后的通配符„_‟可以没有。它使每个成员 p.xi 在未限定的名称 yi 下可

用。例如每个 import 选择器 xi=>yi 将 p.xi 重命名为 yi。如果存在最终的通配符,p

的除 x1,…,xn 之外的成员 z 也将在其自身未限定的名称下可用。

     import 选 择 器 对 类 型 和 术 语 成 员 起 同 样 作 用 。 例 如 , import 子 句 import

p.{x=>y}将术语 p.x 重命名为术语 y,并且将类型名 p.x 重命名为类型名 y。这两个名

称中至少有一个引用 p 的一个成员。

     如果 import 选择器的目标是通配符,import 选择器就会隐藏对源成员的访问。例

如,import 选择器 x=>_将 x“重命名”为通配符号(作为用户程序中的名称不可访问),

因此也有效阻止了对 x 的非限制性的访问。这在同一个 import 选择器列表最后有一个通

配符的情况下是有用的,此时将引入所有前面 import 选择器没有提及的成员。

37


   由 import 子句所引入的绑定的域开始于 import 子句之后并扩展至封闭块,模板,

包子句,或编译单元的末尾,具体决定于哪个先出现。

     存在一些简化形式。import 选择器可以只是一个名字 x。这种情况下,x 以没有重

命名的方式被引入,因此该 import 选择器等价于 x=>x。更进一步,也可以用一个标识

符或通配符来替换整个的 import 选择器列表。import 子句 import p.x 等价于

import p.{x},例如不用限定 p 的成员 x 即可用。import 子句 p._等价于 import

p.{_},例如不用限定 p 的所有成员 x 即可用(该处是 java 中 import p.*的同义语)。

     一个 import 子句中的多个 import 表达式 import p1.I1,…,pn.In 被解释为一个

import 子句的序列 import p1.I1;…;import pn.In。

示例 4.7.1 考虑以下对象定义:

object M{
      def z = 0, one = 1
      def add(x: Int, y: Int):Int = x + y
}


因此代码块

{import M.{one, z => zero, _}; add(zero, one)}


就等价于代码块

{M.add(M.z, M.one)}




赞(0)    操作        顶端 
总帖数
1
每页帖数
101/1页1
返回列表
发新帖子
请输入验证码: 点击刷新验证码
您需要登录后才可以回帖 登录 | 注册
技术讨论