Scala中Trait有什么作用

发布时间:2021-12-09 09:16:35 作者:iii
来源:亿速云 阅读:122

这篇文章主要介绍“Scala中Trait有什么作用”,在日常操作中,相信很多人在Scala中Trait有什么作用问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Scala中Trait有什么作用”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

Inside Scala - 1:Partially applied functions

Partially applied function(不完全应用的函数)是scala中的一种curry机制,本文将通过一个简单的实例来描述在scala中 partially applied function的内部机制。

// Test3.scala  package test   object Test3 {     def sum(x:Int, y:Int, z:Int) = x + y + z        def main(args: Array[String]) {      val sum1 = sum _      val sum2 = sum(1, _:Int, 3)      println(sum1(1,2,3))      println(sum2(2))      List(1,2,3,4).foreach(println);      List(1,2,3,4).foreach(println _)    }   }

在这个代码中 sum _ 表示了一个 新的类型为 (Int,Int,Int)=>Int 的函数,实际上,Scala 会生成一个新的匿名函数(是一个函数对象,Function3),这个函数对象的apply方法会调用 sum 这个对象方法(在这里,是方法,而不是一个函数)。
sum2 是一个 Int => Int的函数(对象),这个函数的apply方法会调用 sum 对象方法。
后面的两行代码都需要访问 println, println是在在Predef对象中定义的方法,在scala中,实际上都会生成一个临时的函数对象,来包装对 println 方法的调用。如果研究一下scala生成的代码,那么可以发现,目前生成的代码中, 对 println, println _生成的代码是重复的,这也说明,目前,所有的你匿名函数基本上没有进行重复性检查。(这可能导致编译生成的的类更大)。

从这里可以得知,虽然,在语法层面,方法(所有的def出来的东西)与函数看起来是一致的,但实际上,二者在底层有区别,方法仍然是不可以直接定位、传值的,他不是一个对象。而仅仅是JVM底层可访问的一个实体。而函数则是虚拟机层面的一个对象。任何从方法到函数的转换,Scala会自动生成一个匿名的函数对象,来进行相应的转换。

所以, List(1,2,3,4).foreach(println) 在底层执行时,并不是获得了一个println的引用(实际上,根本不存在println这个可访问的对象),而是scala自动产生一个匿名的函数,这个函数会调用println。

当然,将一个函数传递时,Scala是不会再做不必要的包装的,而是直接传递这个函数对象了。

Inside Scala - 2: Curry Functions

Curry,在函数式语言中是很常见的,在scala中,对其有特别的支持。

package test

object TestCurry {     def sum(x:Int)(y:Int)(z:Int) = x + y + z     def main(args: Array[String]){       val sum1: (Int => Int => Int) = sum(1)      val sum12: Int => Int = sum(1)(2)      val sum123 = sum(1)(2)(3)      println(sum1(2)(3))      println(sum12(3))      println(sum123)     }   }

在这个例子中, sum 被设计成为一个curried函数,(多级函数?),研究一个函数的实现是很有意思的:

如果看生成的 sum 函数代码,那么,它与 如下编写的
def sum(x:Int, y:Int: z:Int) = x + y + z 是一致的。
而且,如果,你调用sum(1)(2)(3),实际上,scala也并不会产生3次函数调用,而是一次 sum(1,2,3)

也就是说,如果你没有进行 sum(1), sum(1)(2)等调用,那么实际上,上述的代码中根本不会生成额外的函数处理代码。但是,如果我们需要进行一些常用的curry操作时,scala为我们提供了额外的语法级的便利。

Inside Scala - 3: How Trait works

Scala中Trait应该是一个非常强大,但又有些复杂的概念,至少与我,我对trait总是有一些不太明了的地方,求人不如求己,对这些疑问还是自己动手探真的比较好。

还是从一个简单的实例着手。

package test

import java.awt.Point   object TestTrait {     trait Rectangular {      def topLeft: Point      def bottomRight: Point            def left = topLeft.x      def top = topLeft.y      def right = bottomRight.x      def bottom = bottomRight.y      def width = right - left      def height = bottom - top    }        class Rectangle(val topLeft: Point, val bottomRight: Point) extends Rectangular {      override def toString = "I am a rectangle"   }   }

对这段代码,我想问如下的几个问题:
Rectangle是如何继承 Rectangular的行为,如 left, right, width, height的?
Rectangular 对应于Java的接口,那么,相关的实现代码又是如何保存的?
其实,这两个问题是相关的。研究这个问题的最直接的办法莫过于直接分析scalac编译后的结果。
这个类编译后包括:
TestTrait.class 这个类
TestTrait$.class 其实就是 object TestTrait这个对象的类。一个object实际上从属于一个类,scala是对其加后缀$
在这个例子中,TestTrait这个对象实际上并未定义新的属性和方法,因此,并没有包含什么内容
TestTrait$Rectangular.class
对应于代码中的Rectangular这个trait,这实际上是一个接口类。对应的就是这个trait中定义的全部方法。包括topLeft, bottomRight以及后续的实现方法left, width等的接口定义

public interface test.TestTrait$Rectangular extends scala.ScalaObject{   public abstract int height();  public abstract int width();  public abstract int bottom();  public abstract int right();  public abstract int top();  public abstract int left();  public abstract java.awt.Point bottomRight();  public abstract java.awt.Point topLeft();   }

TestTrait$Rectangular$class.class
这个类实际上是trait逻辑的实现类。由于JVM中,接口是不支持任何的实现代码的,因此,scala将相关的逻辑代码编译在这个类中

public abstract class test.TestTrait$Rectangular$class extends java.lang.Object{   public static void $init$(test.TestTrait$Rectangular); // 在这个例子中,没有trait的初始化相关操作    Code:     0:   return  public static int height(test.TestTrait$Rectangular);   // 对应于height = bottom - top这个操作的实现    Code:     0:   aload_0     1:   invokeinterface #17,  1; //InterfaceMethod test/TestTrait$Rectangular.bottom:()I     6:   aload_0     7:   invokeinterface #20,  1; //InterfaceMethod test/TestTrait$Rectangular.top:()I     12:  isub     13:  ireturn

更多的方法并不在此罗列。
首先,这个实现类是抽象的,它不需要被实例化。
所有的trait方法,其实接收一个额外的参数,即 this 对象。对对象的任何的访问,如bottom等操作,实际上是直接调用对象的相应操作。
所有的trait方法,都是static的。
TestTrait$Rectangle.class
这个就是Rectangle这个类的代码了。

// 首先,实现类以implements的方式继承了trait所定义的接口。  public class test.TestTrait$Rectangle extends java.lang.Object implements test.TestTrait$Rectangular,scala.ScalaObject{   // 类的val属性直接对应于一个同名的private字段和相应的读取方法。  private final java.awt.Point bottomRight;   private final java.awt.Point topLeft;   // scala对象比较特殊的是,相应字段的初始化比调用父类构造函数来得更早。也就是说,在Class(arg)中的参数是最早被初始化的。  // 在构造函数后,可以看到,会调用trait的初始化代码。当然,在我们的这个例子中,trait没有任何的初始化行为。  public test.TestTrait$Rectangle(java.awt.Point, java.awt.Point);    Code:     0:   aload_0     1:   aload_1     2:   putfield        #13; //Field topLeft:Ljava/awt/Point;     5:   aload_0     6:   aload_2     7:   putfield        #15; //Field bottomRight:Ljava/awt/Point;     10:  aload_0     11:  invokespecial   #20; //Method java/lang/Object."":()V     14:  aload_0     15:  invokestatic    #26; //Method test/TestTrait$Rectangular$class.$init$:(Ltest/TestTrait$Rectangular;)V     18:  return  // height这个函数是从trait中继承的,在这里,继承体现为对trait实现类的一个调用,同时,将对象本身作为this传递给该函数  public int height();    Code:     0:   aload_0     1:   invokestatic    #39; //Method test/TestTrait$Rectangular$class.height:(Ltest/TestTrait$Rectangular;)I     4:   ireturn

这里不再罗列其他的函数实现,其基本与height函数是相一致的。

理解了以上的逻辑,trait是如何实现将接口和接口实现溶于一体的,应该就非常的清楚了。我以前一直在纳闷一个问题:接口中不能够包含实现代码,那么,难道每次编译继承trait的类时,这写实现的代码是怎么在子类中继承的呢?难道是编译器将这个逻辑复制了一份?如果这样,不仅生成的代码量很大,而且,还有一个问题,那就是,在编译时需要有trait的源代码才行。经过上面的剖析,我们终于知道scala其实有更***的解决之道的:那就是一个trait辅助类。

Inside Scala - 4: Trait Stacks

这个例子摘自 Programming In Scala 这本书第12.5节。本文将从另外一个角度来分析 Stackable Trait的内部原理。

package test

import scala.collection.mutable.ArrayBuffer   object Test7 {     abstract class IntQueue {      def put(x:Int)      def get(): Int    }        class BasicIntQueue extends IntQueue {      private val buf = new ArrayBuffer[Int]      def put(x:Int) { buf += x }      def get() = buf.remove(0)    }     trait Doubling extends IntQueue {      abstract override def put(x:Int) { super.put(2*x) }    }     def main(args: Array[String]) {      val queue: IntQueue = new BasicIntQueue with Doubling      queue.put(1)      queue.put(5)      println( queue.get )      println( queue.get )    }   }

我们来看这一行代码 val queue = new BasicIntQue with Doubling,Scala针对这一行代码干了很多很多的工作,并不是一个简单的操作那么简单
Scala需要新生成一个类型,在我的环境中,这个类叫做:Test7$$anon$1,看看这个代码:
// 新的类以BasicIntQueue为父类,同时实现了Doubling这个trait定义的接口

public final class test.Test7$$anon$1 extends test.Test7$BasicIntQueue implements test.Test7$Doubling{   public test.Test7$$anon$1();    Code:     0:   aload_0     1:   invokespecial   #10; //Method test/Test7$BasicIntQueue."":()V // 父类初始化     4:   aload_0     5:   invokestatic    #16; //Method test/Test7$Doubling$class.$init$:(Ltest/Test7$Doubling;)V // trait辅助类初始化     8:   return  public void put(int);    Code:     0:   aload_0     1:   iload_1     2:   invokestatic    #21; //Method test/Test7$Doubling$class.put:(Ltest/Test7$Doubling;I)V // 这个类使用的是Doubling提供的版本     5:   return  public final void test$Test7$Doubling$$super$put(int); // Doubling所需要的super的版本    Code:     0:   aload_0     1:   iload_1     2:   invokespecial   #29; //Method test/Test7$BasicIntQueue.put:(I)V     5:   return  }

我们来分析一下Doubling这个trait的实现

public interface test.Test7$Doubling extends scala.ScalaObject{   public abstract void put(int);  // 这个是trait中实现的方法   public abstract void test$Test7$Doubling$$super$put(int); // 这个是这个trait 额外依赖的方法   }   // Doubling这个trait的辅助类  public abstract class test.Test7$Doubling$class extends java.lang.Object{  public static void $init$(test.Test7$Doubling);    Code:     0:   return  public static void put(test.Test7$Doubling, int);    Code:     0:   aload_0     1:   iconst_2     2:   iload_1     3:   imul     4:   invokeinterface #17,  2; //InterfaceMethod test/Test7$Doubling.test$Test7$Doubling$$super$put:(I)V  // 这也是 Doubling这个接口中需要 super.init这个方法的原因。     9:   return  }

由此可见,编译器在处理 val queue: IntQueue = new BasicIntQueue with Doubling这一行代码时,需要确定类、Trait的先后顺序。这也是理解Trait的最为复杂的一环。后续,我将就这个问题进行分析。

Inside Scala - 5: Trait Stacks

继续上一个案例,现在我们将Trait的链搞得更长一些:

trait Incrementing extends IntQueue {  abstract override def put(x: Int) { super.put(x + 1) }  }  trait Filtering extends IntQueue {  abstract override def put(x: Int) {  if (x >= 0) super.put(x)  }  }   val queue: IntQueue = new BasicIntQueue with Incrementing with Filtering

新的类如何呢?当我们调用 queue的 put方法时,这个的先后顺序究竟如何呢?还是看看生成的代码:

public final class test.Test7$$anon$1 extends test.Test7$BasicIntQueue implements test.Test7$Incrementing,test.Test7$Filtering{   // 初始化的顺序:先父类、再Incremeting、再Filtering,这个顺序与源代码的顺序是一致的。  public test.Test7$$anon$1();    Code:     0:   aload_0     1:   invokespecial   #10; //Method test/Test7$BasicIntQueue."":()V     4:   aload_0     5:   invokestatic    #16; //Method test/Test7$Incrementing$class.$init$:(Ltest/Test7$Incrementing;)V     8:   aload_0     9:   invokestatic    #21; //Method test/Test7$Filtering$class.$init$:(Ltest/Test7$Filtering;)V     12:  return  // put 方法实际使用的是 Filtering这个Trait的put  public void put(int);    Code:     0:   aload_0     1:   iload_1     2:   invokestatic    #34; //Method test/Test7$Filtering$class.put:(Ltest/Test7$Filtering;I)V     5:   return  // Filtering Trait的父实现是Incremeting trait  public final void test$Test7$Filtering$$super$put(int);    Code:     0:   aload_0     1:   iload_1     2:   invokestatic    #38; //Method test/Test7$Incrementing$class.put:(Ltest/Test7$Incrementing;I)V     5:   return  // incrementing的父实现是父类的实现。  public final void test$Test7$Incrementing$$super$put(int);    Code:     0:   aload_0     1:   iload_1     2:   invokespecial   #26; //Method test/Test7$BasicIntQueue.put:(I)V     5:   return  }

因此,要理解这个过程,可以这么来分析:val queue: IntQueue = new BasicIntQueue with Incrementing with Filtering
首先初始化的是BasicIntQueue
在这个基础上叠加 Incrementing,super.put引用的是BasicIntQueue的put方法
再在叠加后的基础上叠加 Filtering,super.put引用的是 Incrementing的put方法
叠加后的结果就是***的版本。put引用的是Filtering的put方法
因此,初始化的顺序是从左至右,而方法的可见性则是从右至左(可以理解为上面的叠加关系,叠加之后,上面的trait具有更大的优先可见性。

Inside Scala - 6:Case Class 与 模式匹配

本文将尝试对Case Class是如何参与模式匹配的进行剖析。文中的代码还是来自 Programming In Scala一书。

abstract class Expr;  case class Var(name: String) extends Expr;  case class Number(num: Double) extends Expr;  case class UnOp(operator: String, arg: Expr) extends Expr;  case class BinOp(operator:String, left: Expr, right: Expr) extends Expr;

这里我们先来看一个最为简单的模式匹配

some match {    case Var(name) => println("a var with name:" + name)  }


这几行的代码编译后等效于:

if(some instanceof Var)  {      Var temp21 = (Var)some;      String name = temp21.name();      if(true)      {          name = temp22;          Predef$.MODULE$.println((new StringBuilder()).append("a var with name:").append(name).toString());      } else     {          throw new MatchError(some.toString());      }  } else {      throw new MatchError(some.toString());  }


如果从生成的代码的角度上来看,Scala生成的代码质量并不高,其中的 if(true) else 的那个部分就有明显的废代码。(不过,这个对运行效率的影响到时几乎可以忽略,只是编译后的字节码倒是没理由的多了几分)。
上面的这个模式匹配仅仅是匹配一个类型。因此,其对应的java原语就是 instanceof 检测。

让我们更进一步, 看看如下的例子:

some match {    case Var("x") => println("a var with name:x")  }

这个模式匹配不仅匹配类型,还要匹配构造器中的name属性为 "x"常量。这里我就不在福州 Scala生成的字节码了,而是简单的翻译一下:
if( some instanceof Var)  -- 类型检查
var.name() == "x"             -- 检查 对象的 name 属性是否等于 "x",编译器非常清楚的指导 Case Class的每一个构造参数所对应的字段名称。

更进一步,让我们看看一个更复杂的模式匹配:嵌套的对象。

some match {    case BinOp("+", Var("x"), UnOp("-", Number(num))) => println("x - " + num)  }

这个逻辑其实也是上面的一个嵌套:
some instanceof BinOp
some.operator == "+"  编译器进行了特殊的null检测,以防止这个操作出现NPE
some.left instanceof Var
some.left.name == "x"
some.right instanceof UnOp
some.right.operator == "-"
some.right.arg instanceof Number
......
实际上,Scala的模式匹配确实为我们干了很多很多的事情,这也使得在很多的情况下,使用scala的模式匹配为我们提供了一个非常安全的(不用担心大量的Null检查),以及非常复杂的匹配操作。当然,与更复杂的模式匹配相比(譬如,规则引擎其实也是一个模式匹配的引擎),Scala的模式匹配还是相对比较简单的。

这里简单的补充一下 Scala中的几种模式:
1、通配符模式。 也就是说使用 case _ => 来匹配所有的东西。或者,case Var(_) 来对局部进行通配。
2、常量匹配。譬如上述的Var("x") ,其中,"x"就是一个常量。常量除了文字常量外,还可以使用以大写字母开头的scala变量,或者`varname`形式的引用。
3、变量匹配。一个变量匹配实际上匹配任何的类型,并同时赋予其一个变量名。
4、构造函数匹配。匹配一个给定的类型,并且嵌套的对其参数进行匹配。参数可以是通配符模式、常量、变量或者子构造函数匹配
5、对于List类型, _*可以匹配剩余的全部元素。
6、Tuple匹配。(a,b,c)
7、类型匹配。对于java对象,由于并不适合Scala的Case Class模型,因此,可以使用类型进行匹配。在这种情况下,与构造子匹配是不同的。

再摘一段我以前编写的使用scala来编写应用程序的逻辑代码,让我们看看模式匹配在商业应用中的使用:

_req.transType match {        case RechargeEcp | RechargeGnete | FreezeToAvailable => // 充值类交易          assert(_req.amount > 0, "金额不正确")        case DirectPay | AvailableToFreeze =>    // 支付、冻结类交易          assert(_req.amount < 0, "金额不正确")        case _ =>              assert(false, "无效交易类型")      }            val _account = queryEwAccount(_req.userId)      assert(_account != null, "用户尚未开通电子钱包")            var _accAvail, _accFreeze: EWSubAccount = null     var _total: BigDecimal = _req.amount      _account.subAccounts.find(_.subTypeCode==Available) match {        case Some(x) =>  _accAvail = x;    _total += x.balance        case None =>      }      _account.subAccounts.find(_.subTypeCode==Freeze) match {        case Some(x) =>  _accFreeze = x;    _total += x.balance        case None=>      }

这个仅仅是一个很简单的应用,试想使用Java的if/else或者switch来进行相同的代码,你不妨看看代码量会增加多少?可读性又会如何呢?

Scala Actor是一种借鉴于Erlang的进程消息机制的并发编程模式,由于Java中不存在Erlang的进程的概念,因此,Scala的Actor在隔离性上是不如Erlang的,譬如,在Erlang中,可以有效的终止一个进程,不仅仅无需担心死锁(根本没有锁),也可以马上释放掉改进程的内存,这种隔离性在某种程度上是更接近于操作系统的进程的。在Java的世界里暂时没有等效的替代品。

(题外话,最近在我们的Open Service Platform中集成了一个类似于操作系统定时调度的机制,可以定时执行一些任务,但是***,我们仍然决定将部分非交易相关的定时任务,主要是一些日志分析类、管理性批量处理等定时任务放到操作系统上进行调度,毕竟操作系统提供了一个更好的虚拟机,在OSGi层面仍然是有限的隔离,哪一天JVM能够提供像操作系统的隔离特性,那么,操作系统就真的不重要了)。

本文将对actor的机制进行简单的分析,以帮助加强对actor的理解。

package learn.actor   object Test1 extends Application {     import scala.actors.Actor._     val actor1 = actor {       println("i am in " + Thread.currentThread)       while(true) {         receive {           case msg => println("recieve msg:" + msg + " In " + Thread.currentThread);         }       }     }         val actor2 = actor {       println("i am in " + Thread.currentThread)       while(true) {         receive {           case msg: String => println("recieve msg:" + msg.toUpperCase + " In " + Thread.currentThread);         }       }     }         actor1 ! "Hello World"    actor2 ! "Hello World"    actor1 ! "ok"    actor2 ! "ok"      }

运行的结果是:

i am in Thread[pool-1-thread-1,5,main]  i am in Thread[pool-1-thread-2,5,main]  recieve msg:HELLO WORLD In Thread[pool-1-thread-2,5,main]  recieve msg:Hello World In Thread[pool-1-thread-1,5,main]  recieve msg:OK In Thread[pool-1-thread-2,5,main]  recieve msg:ok In Thread[pool-1-thread-1,5,main]

从这个例子来看,actor1和actor2实际上是两个独立的Java线程,任何线程可以将消息以 ! 的方式发给给这个线程进行处理。由于采用消息的方式来进行通信,因此,线程与线程之间无需采用Java的notify/wait机制,而后者是建立在锁的基础之上的。有关于这一点,我不在本文只进行深入的分析了。(有必要的话,我会再写一个帖子来说明)。

那么 Scala Actor 的底层基础是什么呢?与Java的notify/wait就完全没有关系吗?我们将重点分析actor的三个方法:!, receive, react

1、Scala Actor的send(外部调用者发送一个消息给当前actor)和receive(当前actor接收一个消息),这两个操作是同步的(synchronized),也就是说,不可同时进入。(客观的说,这一块应该有很大的优化空间,应该采用乐观锁的机制,可能会有更好的效率,一来,send/receive操作本身都是很快速的操作,即便在出现冲突的情况下,使用乐观锁也可以降低线程切换引起的开销,而且,在大部分情况下,send操作与receive操作引发冲突的可能性并不是很大的。也就是说,在很大的程度上,send和receive还可以有更好的并行性,不知道后续的scala版本是否会进行优化。)

2、执行send操作时,如果当前actor正在等待这个消息(指actor自身已经在receive、react并且期待这个消息的情况下),那么原来的等待将会马上执行,否则,消息会进入到actor的邮箱,等待下次receive/react的处理。这种模式相较于全部放入邮箱更加有效。它避免了一次在邮箱上的同步等待。

3、当执行receive操作时,actor会检查对象的邮箱,如果有匹配的消息的话,则会马上返回该消息进行处理,否则会处在等待状态(当前线程阻塞,采用的是wait原语)当匹配的消息到达时,也是采用notify原语通知等待线程继续actor的处理的。

4、react与receive不同的是,react从不返回。这个在Java的编程世界里,好像还没有看到类似的东西,该如何理解它呢:

react(f: ParticialFunction[Any,Unit]) 首先检查actor的邮箱,如果有符合f的消息,则马上提取该消息,并且在一个ExecutionPool中调度执行f。(因此,f的执行肯定不在请求react这个线程中执行的。当前的调用react的线程,将产生一个 SuspendActorException,从而中断一般的执行过程。(也就是说文档中说的不返回的概念)

如果当前邮箱中没有消息,react将登记一个Continuation对象,将等待的消息(一个等待给定消息的函数)、获得消息后需要继续进行的处理在actor中进行登记,而后,当前线程会产生一个SuspendActorException,中断处理(从而是将当前线程归还到线程池)。

当消息到达(通过send)时,send将检查等待消息的Continuation,如过匹配的话,则会在线程池中的选择一个线程来执行f函数。在f处理完成一个消息后,一般的,它会再次调用 react来处理下一个消息,将再次重复这个过程。

应该说,scala的这个设计是非常精巧,也非常有效的,但这对Java开发程序员来说,就意味着一个新的挑战:看上去的一个函数体,实际上其中的代码不仅是执行不连续的(如closure可能会延迟、重复多次的被调用),甚至可能是在不同的线程中被执行的。

从这个概念上来看,scala的actor并不对应于Java的线程,相反,可以理解为一个行为执行者,是一个有上下文的非操作系统线程,语义其实更接近于现实的一个载体。这个与Erlang的进程还是有很明显的语义上的区别的。从上述的分析中,或许如果切换到乐观锁的机制,Scala的并发效率还能有更进一步的提升。

到此,关于“Scala中Trait有什么作用”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!

推荐阅读:
  1. PHP中Trait的作用和优点是什么
  2. PHP中Trait有什么用

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

scala trait

上一篇:miRNA靶基因查询R包和网页工具该怎么使用

下一篇:Scala过度包装的平衡怎么理解

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》