1. 程式人生 > >快學Scala學習筆記及習題解答(10-11特質與操作符)

快學Scala學習筆記及習題解答(10-11特質與操作符)

本文Scala使用的版本是2.11.8

第10章 特質

10.1 基本使用

特質可以同時擁有抽象方法和具體方法,而類可以實現多個特質。

import java.util.Date

trait AbsLogged {
    // 特質中未被實現的方法預設就是抽象的.
    def log(msg: String)
}

trait Logged extends AbsLogged {
    // 重寫抽象方法, 此處為空實現
    override def log(msg: String) { }
}

trait ConsoleLogger extends Logged {
    override
def log(msg: String) { println("ConsoleLogger: " + msg) } } trait FileLog extends Logged { override def log(msg: String) { println("FileLog: " + msg) } } trait TimeLog extends Logged { override def log(msg: String): Unit = { super.log(new Date() + " " + msg) } } trait ShortLogger
extends Logged {
val maxLength = 15 override def log(msg: String): Unit = { super.log( if (msg.length < maxLength) msg else msg.substring(0, maxLength - 3) + "..." ) } } // 如果需要的特質不止一個,可以用with關鍵字新增額外的特質 class SavingsAccount extends Logged with Cloneable with
Serializable {
def withdraw(amount: Double): Unit = { if (amount > 10.0) log("Insufficient funds") else log("else") } } // 測試 object Test { def main(args: Array[String]) { // 如下可以混入不同的特質 val acct1 = new SavingsAccount with ConsoleLogger acct1.log("acct1") val acct2 = new SavingsAccount with FileLog acct2.log("acct2") // 可以疊加多個特質, 一般從最後一個開始被處理 val acct3 = new SavingsAccount with ConsoleLogger with TimeLog with ShortLogger acct3.withdraw(11) val acct4 = new SavingsAccount with ConsoleLogger with ShortLogger with TimeLog acct4.withdraw(11) } } // 執行結果 ConsoleLogger: acct1 FileLog: acct2 ConsoleLogger: Tue Nov 22 07:10:15 CST 2016 Insufficient... ConsoleLogger: Tue Nov 22 0...

10.2 當做富介面使用的特質

特質可以包含大量工具方法,而這些方法可以依賴一些抽象方法來實現。

trait Logger {
    def log(msg: String)
    def info(msg: String) { log("INFO: " + msg) }
    def warn(msg: String) { log("WARN: " + msg) }
    def error(msg: String) { log("ERROR: " + msg) }
}

10.3 特質中的欄位

給出初始值的是具體欄位;否則為抽象欄位,子類必須提供該欄位。
這些欄位不是被子類繼承,而是簡單地被加到子類中。

10.4 特質的構造順序

和類一樣,特質也可以有構造器,由欄位的初始化和其他特質中的語句構成。

構造器以如下順序執行:

  1. 首先呼叫超類的構造器。
  2. 特質構造器在超類構造器之後、類構造器之前執行。
  3. 特質由左到右被構造。
  4. 每個特質當中,父特質先被構造。
  5. 如果多個特質共用一個父特質,而這個父特質已經被構造,則不會再次構造。
  6. 所有特質構造完畢,子類被構造。

10.5 初始化特質中的欄位

特質不能有構造器引數。每個特質都有一個無引數的構造器。而且構造順序的問題,在子類中初始化特質的欄位,可能會有陷阱。

import java.io.PrintStream

trait Logger {
    def log(msg: String) {}
    def info(msg: String) { log("INFO: " + msg) }
    def warn(msg: String) { log("WARN: " + msg) }
    def error(msg: String) { log("ERROR: " + msg) }
}

trait FileLogger extends Logger {
    val fileName: String
    val out = new PrintStream(fileName)

    override def log(msg: String) { out.println(msg); out.flush() }
}

class SavingsAccount2 extends Logger {
    def withdraw(amount: Double): Unit = {
        if (amount > 10.0) log("Insufficient funds")
        else log("else")
    }
}

// 類的提前定義
class TestAccount extends {
    val fileName = "test.log"
} with SavingsAccount2 with FileLogger

object Test2 {

    def main(args: Array[String]) {
        // 特質的提前定義
        val acct = new {
            val fileName = "myapp.log"
        } with SavingsAccount2 with FileLogger
        acct.withdraw(11)

        // 類的提前定義
        val test = new TestAccount
        test.withdraw(1)
    }
}

另一種是在FileLogger構造器中使用懶值:

lazy val out = new PrintStream(fileName)

10.6 擴充套件類的特質

// 特質可以擴充套件類
trait LoggedException extends Exception with Logger {
    def log() { log(getMessage) }
}

class UnhappyException extends LoggedException {
    override def getMessage = "arggh!"
}

// 如果類已經擴充套件了另一個類, 只要這個類是特質的超類的一個子類就可以
class UnhappyException2 extends IOException with LoggedException {
    override def getMessage = "UnhappyException2!"
}

object Test2 {

    def main(args: Array[String]) {

        val ex = new UnhappyException
        ex.log()

        val ex2 = new UnhappyException2
        ex2.log()
    }
}

10.7 自身型別

當特質以如下程式碼開始定義時

this: 型別 =>

它便只能被混入指定型別的子類。

trait LoggedException2 extends Logged {
    this: Exception =>
        def log() { log(getMessage) }
}

下面這種特質可以被混入任何擁有getMessage方法的類。

trait LoggedException3 extends Logged {
    this: { def getMessage(): String } =>

    def log() { log(getMessage()) }
}

10.8 習題解答


1. java.awt.Rectangle類有兩個很有用的方法translate和grow,但可惜的是像java.awt.geom.Ellipse2D這樣的類中沒有。在Scala中,你可以解決掉這個問題。定義一個RectangleLike特質,加入具體的translate和grow方法。提供任何你需要用來實現的抽象方法,以便你可以像如下程式碼這樣混入該特質:

val egg = new java.awt.geom.Ellipse2D.Double(5, 10, 20, 30) with RectangleLike
egg.translate(10, -10)
egg.grow(10, 20)
package com.zw.demo.tenth

import java.awt.geom.Ellipse2D

trait RectangleLike {
  this:Ellipse2D.Double =>
  def translate(dx : Int, dy : Int): Unit = {
    this.x += dx
    this.y += dy
  }
  def grow(h : Int, v : Int): Unit = {
    this.width = v
    this.height = h
  }
}

// 測試類
package com.zw.demo.tenth

object One {

  def main(args: Array[String]): Unit = {
    val egg = new java.awt.geom.Ellipse2D.Double(5, 10, 20, 30) with RectangleLike

    println("x = " + egg.getX + " y = " + egg.getY)
    egg.translate(10, -10)
    println("x = " + egg.getX + " y = " + egg.getY)

    println("w = " + egg.getWidth + " h = " + egg.getHeight)
    egg.grow(10, 21)
    println("w = " + egg.getWidth + " h = " + egg.getHeight)
  }
}

// 結果
x = 5.0 y = 10.0
x = 15.0 y = 0.0
w = 20.0 h = 30.0
w = 21.0 h = 10.0


2. 通過把scala.math.Ordered[Point]混入java.awt.Point的方式,定義OrderedPoint類。按辭典編輯方式排序,也就是說,如果x

package com.zw.demo.tenth

import java.awt.Point

class OrderedPoint(
                  x:Int,
                  y:Int
                  ) extends Point(x:Int, y:Int) with Ordered[Point]{

  override def compare(that: Point): Int = {
    if (this.x <= that.x && this.y < that.y) -1
    else if (this.x == that.x && this.y == that.y) 0
    else 1
  }
}

// 測試類
package com.zw.demo.tenth

object Two {

  def main(args: Array[String]) {
    val arr : Array[OrderedPoint] = new Array[OrderedPoint](3)
    arr(0) = new OrderedPoint(4,5)
    arr(1) = new OrderedPoint(2,2)
    arr(2) = new OrderedPoint(4,6)
    val sortedArr = arr.sortWith(_ > _)
    sortedArr.foreach((point:OrderedPoint) => println("x = " + point.getX + " y = " + point.getY))
  }

}

// 結果
x = 4.0 y = 6.0
x = 4.0 y = 5.0
x = 2.0 y = 2.0


3. 檢視BitSet類,將它的所有超類和特質繪製成一張圖。忽略型別引數([…]中的所有內容)。然後給出該特質的線性化規格說明。

類圖

Sorted、SetLike、SortedSetLike、Set、SortedSet、BitSetLike、BitSet


4. 提供一個CryptoLogger類,將日誌訊息以凱撒密碼加密。預設情況下金鑰為3,不過使用者可以重寫它。提供預設金鑰和-3作為金鑰時的使用示例。

package com.zw.demo.tenth

trait CryptoLogger {
  def crypto(str : String, key : Int = 3) : String = {
    for ( i <- str) yield
      if (key >= 0) (97 + ((i - 97 + key)%26)).toChar
      else (97 + ((i - 97 + 26 + key)%26)).toChar
  }
}

// 測試類
package com.zw.demo.tenth

/**
  * Created by zhangws on 16/10/28.
  */
object Three {

  def main(args: Array[String]) {
    val log = new CryptoLogger {}

    val plain = "abcdef"
    println("明文為:" + plain)
    println("加密後為:" + log.crypto(plain))
    println("加密後為:" + log.crypto(plain, -3))
  }
}

// 結果
明文為:abcdef
加密後為:defghi
加密後為:xyzabc


5. JavaBeans規範裡有一種提法叫做屬性變更監聽器(property change listener),這是bean用來通知其屬性變更的標準方式。PropertyChangeSupport類對於任何想要支援屬性變更監聽器的bean而言是個便捷的超類。但可惜已有其他超類的類——比如JComponent——必須重新實現相應的方法。將PropertyChangeSupport重新實現為一個特質,然後將它混入到java.awt.Point類中。

package com.zw.demo.tenth

import java.beans.PropertyChangeSupport

trait PropertyChange {
  val propertyChangeSupport : PropertyChangeSupport
}

// 測試
package com.zw.demo.tenth

import java.awt.Point
import java.beans.{PropertyChangeSupport, PropertyChangeEvent, PropertyChangeListener}

object Five {

  def main(args: Array[String]) {
    val p = new Point() with PropertyChange {
      val propertyChangeSupport = new PropertyChangeSupport(this)
      propertyChangeSupport.addPropertyChangeListener(new PropertyChangeListener {
        override def propertyChange(evt: PropertyChangeEvent): Unit = {
          println(evt.getPropertyName
            + ": oldValue = " + evt.getOldValue
            + " newValue = " + evt.getNewValue)
        }
      })
    }
    val newX : Int = 20
    p.propertyChangeSupport.firePropertyChange("x", p.getX, newX)
    p.move(newX, 30)
  }
}

// 結果
x: oldValue = 0.0 newValue = 20


6. 在Java AWT類庫中,我們有一個Container類,一個可以用於各種元件的Component子類。舉例來說,Button是一個Component,但Panel是Container。這是一個運轉中的組合模式。Swing有JComponent和JContainer,但如果你仔細看的話,你會發現一些奇怪的細節。儘管把其他元件新增到比如JButton中毫無意義,JComponent依然擴充套件自Container。Swing的設計者們理想情況下應該會更傾向於圖10-4中的設計。但在Java中那是不可能的。請解釋這是為什麼?Scala中如何用特質來設計出這樣的效果?

Java只能單繼承,JContainer不能同時繼承自Container和JComponent。Scala可以通過特質解決這個問題.


7. 做一個你自己的關於特質的繼承層級,要求體現出疊加在一起的特質、具體的和抽象的方法,以及具體的和抽象的欄位。

package com.zw.demo.tenth

trait Fly{
  def fly(){
    println("flying")
  }

  def flywithnowing()
}

trait Walk{
  def walk(){
    println("walk")
  }
}

class Bird{
  var name:String = _
}

class BlueBird extends Bird with Fly with Walk{
  def flywithnowing() {
    println("BlueBird flywithnowing")
  }
}

object Seven {

  def main(args: Array[String]) {
    val b = new BlueBird()
    b.walk()
    b.flywithnowing()
    b.fly()
  }
}


8. 在java.io類庫中,你可以通過BufferedInputStream修飾器來給輸入流增加緩衝機制。用特質來重新實現緩衝。簡單起見,重寫read方法。

import java.io.{FileInputStream, InputStream}

trait Buffering {
    this: InputStream =>

    val BUF_SIZE: Int = 5
    val buf: Array[Byte] = new Array[Byte](BUF_SIZE)
    var bufsize: Int = 0 // 快取資料大小
    var pos: Int = 0 // 當前位置

    override def read(): Int = {
        if (pos >= bufsize) { // 讀取資料
            bufsize = this.read(buf, 0, BUF_SIZE)
            if (bufsize <= 0) return bufsize
            pos = 0
        }
        pos += 1 // 移位
        buf(pos - 1) // 返回資料
    }
}

object Eight {

    def main(args: Array[String]) {
        val f = new FileInputStream("myapp.log") with Buffering
        for (i <- 1 to 30) println(f.read())
    }
}


9. 使用本章的日誌生成器特質,給前一個練習中的方案增加日誌功能,要求體現出緩衝的效果。

import java.io.{FileInputStream, InputStream}

trait Logger {
    def log(msg: String)
}

trait PrintLogger extends Logger {
    def log(msg: String) = println(msg)
}

trait Buffering {
    this: InputStream with Logger =>

    val BUF_SIZE: Int = 5
    val buf: Array[Byte] = new Array[Byte](BUF_SIZE)
    var bufsize: Int = 0 // 快取資料大小
    var pos: Int = 0 // 當前位置

    override def read(): Int = {
        if (pos >= bufsize) { // 讀取資料
            bufsize = this.read(buf, 0, BUF_SIZE)
            if (bufsize <= 0) return bufsize
            log("buffered %d bytes: %s".format(bufsize, buf.mkString(", ")))
            pos = 0
        }
        pos += 1 // 移位
        buf(pos - 1) // 返回資料
    }
}

object Eight {

    def main(args: Array[String]) {
        val f = new FileInputStream("myapp.log") with Buffering with PrintLogger
        for (i <- 1 to 20) println(f.read())
    }
}

// 結果
buffered 5 bytes: 73, 110, 115, 117, 102
73
110
115
117
102
buffered 5 bytes: 102, 105, 99, 105, 101
102
105
99
105
101
buffered 5 bytes: 110, 116, 32, 102, 117
110
116
32
102
117
buffered 4 bytes: 110, 100, 115, 10, 117
110
100
115
10
-1


10. 實現一個IterableInputStream類,擴充套件java.io.InputStream並混入Iterable[Byte]特質。

import java.io.{FileInputStream, InputStream}

/**
  * Created by zhangws on 16/10/28.
  */
trait IterableInputStream extends InputStream with Iterable[Byte] {

    class InputStreamIterator(outer: IterableInputStream) extends Iterator[Byte] {
        def hasNext: Boolean = outer.available() > 0

        def next: Byte = outer.read().toByte
    }

    override def iterator: Iterator[Byte] = new InputStreamIterator(this)
}

object Ten extends App {
    val fis = new FileInputStream("myapp.log") with IterableInputStream
    val it = fis.iterator
    while (it.hasNext)
        println(it.next())
}
fis.close()

10.9 參考

第11章 操作符

11.1 基本使用

yield在Scala中是保留字,如果訪問Java中的同名方法,可以用反引號

Thread.`yield`()

中置操作符

1 to 10 相當於 1.to(10)
1 -> 10 相當於 1.->(10)

使用操作符的名稱定義方法就可以實現定義操作符

class Fraction(n: Int, d: Int) {
    val num: Int = n
    val den: Int = d
    def *(other: Fraction) = new Fraction(num * other.num, den * other.den)

    override def toString = {
        "num = " + num + "; den = " + den
    }
}

object StudyDemo extends App {
    val f1 = new Fraction(2, 3)
    val f2 = new Fraction(4, 5)
    val f3 = f1 * f2
    println(f3)
}

一元操作符

# 後置操作符
1 toString 等同於 1.toString()

# 前置操作符(例如+、-、!、~)
-a 轉換為unary_操作符的方法呼叫 a.unary_-

賦值操作符

a 操作符= b 等同於 a = a 操作符 b
例如:a += b 等同於 a = a + b


1. <=、>=和!=不是賦值操作符;
2. 以=開頭的操作符不是賦值操作符(==、===、=/=等);
3. 如果a有一個名為操作符=的方法,那麼該方法會被直接呼叫。

結合性

除了以下操作符,其他都是左結合的

1. 以冒號(:)結尾的操作符;
2. 賦值操作符。

例如

2 :: Nil 等同於Nil.::(2)

11.2 優先順序

除複製操作符外,優先順序由操作符的首字元決定。

最高優先順序:除以下字元外的操作符字元
* / %
+ -
:
< >
! =
&
^
|
非操作符
最低優先順序:賦值操作符


後置操作符優先順序低於中置操作符:

a 中置操作符 b 後置操作符
等價於
(a 中置操作符 b) 後置操作符

11.3 apply和update方法

f(arg1, arg2, ...)

如果f不是函式或方法,等同於

f.apply(arg1, arg2, ...)

表示式

f(arg1, arg2, ...) = value

等同於

f.update(arg1, arg2, ..., value)

apply常被用在伴生物件中,用來構造物件而不用顯示地使用new。

class Fraction(n: Int, d: Int) {
    ...
}

object Fraction {
    def apply(n: Int, d: Int) = new Fraction(n, d)
}

val result = Fraction(3, 4) * Fraction(2, 5

11.4 提取器

所謂提取器就是一個帶有unapply方法的物件。可以當做伴生物件中apply方法的反向操作。

unapply接受一個物件,然後從中提取值。

val author = "Cay Horstmann"
val Name(first, last) = author // 呼叫Name.unapply(author)

object Name {
    def unapply(input: String) = {
        val pos = input.indexOf(" ")
        if (pos == -1) None
        else Some((input.substring(0, pos), input(substring(pos + 1))
    }
}

每一個樣例類都自動具備apply和unapply方法。

case class Currency(value: Double, unit: String)

帶單個引數的提取器

object Number {
    def unapply(input: String): Option[Int] = {
        try {
            Some(Integer.parseInt(input.trim))
        } catch {
            case ex: NumberFormatException => None
        }
    }
}

val Number(n) = "1223"

無引數的提取器

object IsCompound {
    def unapply(input: String) = input.contains(" ")
}

author match {
    // 作者Peter van der也能匹配
    case Name(first, last @ IsCompound()) => ...
    case Name(first, last) => ...
}

unapplySeq方法,提取任意長度的值得序列。

object Name {
    def unapplySeq(input: String): Option[Seq[String]] = {
        if (input.trim == "") None
        else Some(input.trim.split("\\s+"))
    }
}

// 這樣就可以匹配任意數量的變量了

author math {
    case Name(first, last) => ...
    case Name(first, middle, last) => ...
    case Name(first, "van", "der", last) => ...
}

11.5 習題解答


1. 根據優先順序規則,3+4->5和3->4+5是如何被求值的?

scala> 3 + 4 -> 5
res0: (Int, Int) = (7,5)

scala> 3 -> 4 + 5
<console>:12: error: type mismatch;
 found   : Int(5)
 required: String
       3 -> 4 + 5
                ^


2. BitInt類有一個pow方法,但沒有用操作符字元。Scala類庫的設計者為什麼沒有選用**(像Fortran那樣)或者^(像Pascal那樣)作為乘方操作符呢?

因為優先順序問題,在scala中*優先於^,但數學中乘方優先於乘法。所以沒有提供^作為乘方的操作符。


3. 實現Fraction類,支援 + - * / 操作。支援約分,例如將15/-6變成-5/2。除以最大公約數,像這樣:

class Fraction(n: Int, d: Int) {
  private val num: Int = if (d == 0) 1 else n * sign(d) /gcd(n, d);
  private val den: Int = if (d == 0) 0 else d * sign(d) /gcd(n, d);
  override def toString = num + "/" + den
  def sign(a: Int) = if (a > 0) 1 else if (a < 0) -1 else 0
  def gcd(a: Int, b: Int): Int = if (b == 0) abs(a) else gcd(b, a % b)
  ...
}
import scala.math.abs

class Fraction(n: Int, d: Int) {
    private val num: Int = if (d == 0) 1 else n * sign(d) / gcd(n, d)
    private val den: Int = if (d == 0) 0 else d * sign(d) / gcd(n, d)

    override def toString = num + "/" + den

    // 正負號
    def sign(a: Int): Int = if (a > 0) 1 else if (a < 0) -1 else 0

    // 最大公約數
    def gcd(a: Int, b: Int): Int = if (b == 0) abs(a) else gcd(b, a % b)

    def +(other: Fraction): Fraction = {
        Fraction((this.num * other.den) + (other.num * this.den), this.den * other.den)
    }

    def -(other: Fraction): Fraction = {
        Fraction((this.num * other.den) - (other.num * this.den), this.den * other.den)
    }

    def *(other: Fraction): Fraction = {
        Fraction(this.num * other.num, this.den * other.den)
    }

    def /(other: Fraction): Fraction = {
        Fraction(this.num * other.den, this.den * other.num)
    }
}

object Fraction {
    def apply(n: Int, d: Int) = new Fraction(n, d)
}

object Three extends App {
    val f = new Fraction(15, -6)
    val p = new Fraction(20, 60)
    println(f)
    println(p)
    println(f + p)
    println(f - p)
    println(f * p)
    println(f / p)
}

// 結果
-5/2
1/3
-13/6
-17/6
-5/6
-15/2


4. 實現一個Money類,加入美元和美分欄位。提供+、-操作符以及比較操作符==和<。舉例來說Money(1, 75) + Money(0, 50) == Money(2, 25)應為true。你應該同時提供*和/操作符嗎?為什麼?

金額的乘除沒有實際意義。

class Money(val dollar: Int, val cent: Int) {
    def +(other: Money): Money = {
        Money(this.dollar + other.dollar, this.cent + other.cent)
    }

    def -(other: Money): Money = {
        Money(0, this.toCent - other.toCent)
    }

    def <(other: Money): Boolean = this.dollar < other.dollar || (this.dollar == other.dollar && this.cent < other.cent)

    def ==(other: Money): Boolean = this.dollar == other.dollar && this.cent == other.cent

    private def toCent = this.dollar * 100 + this.cent

    override def toString = { "dollar = " + this.dollar + " cent = " + this.cent}
}

object Money {
    def apply(dollar: Int, cent: Int) = {
        val d = dollar + cent / 100
        new Money(d, cent % 100)
    }
}

object Four extends App {
    val m1 = Money(1, 200)
    val m2 = Money(2, 2)
    println(m1 + m2)
    println(m1 - m2)
    println(m2 - m1)
    println(m1 == m2)
    println(m1 < m2)
    println(Money(1, 75) + Money(0, 50))
    println(Money(1, 75) + Money(0, 50) == Money(2, 25))
}

// 結果
dollar = 5 cent = 2
dollar = 0 cent = 98
dollar = 0 cent = -98
false
false
dollar = 2 cent = 25
true


5. 提供操作符用於構造HTML表格。例如:

Table() | "Java" | "Scala" || "Gosling" | "Odersky" || "JVM" | "JVM, .NET"

應產出

<table><tr><td>Java</td><td>Scala</td></tr><tr><td>Gosling...
class Table {
    var s: String = ""

    def |(str: String): Table = {
        Table(this.s + "<td>" + str + "</td>")
    }

    def ||(str: String): Table = {
        Table(this.s + "</tr><tr><td>" + str + "</td>")
    }

    override def toString: String = {
        "<table><tr>" + this.s + "</tr></table>"
    }
}

object Table {
    def apply(): Table = {
        new Table
    }

    def apply(str: String): Table = {
        val t = new Table
        t.s = str
        t
    }
}

object Five extends App {
    println(Table() | "Java" | "Scala" || "Gosling" | "Odersky" || "JVM" | "JVM,.NET")
}

// 結果
<table><tr><td>Java</td><td>Scala</td></tr><tr><td>Gosling</td><td>Odersky</td></tr><tr><td>JVM</td><td>JVM,.NET</td></tr></table>


6. 提供一個ASCIIArt類,其物件包含類似這樣的圖形:


 /\_/\
( ' ' )
(  -  )
 | | |
(__|__)
提供將兩個ASCIIArt圖形橫向或縱向結合的操作符。選用適當優先順序的操作符命名。縱向結合的例項:
 /\\_/\     -----
( ' ' )  / Hello \
(  -  ) 
import scala.collection.mutable.ArrayBuffer

class ASCIIArt(str: String) {
    val arr: ArrayBuffer[ArrayBuffer[String]] = new ArrayBuffer[ArrayBuffer[String]]()

    if (str != null && !str.trim.equals("")) {
        str.split("[\r\n]+").foreach(
            line => {
                val s = new ArrayBuffer[String]()
                s += line
                arr += s
            }
        )
    }

    def this() {
        this("")
    }

    /**
      * 橫向結合
      * @param other
      * @return
      */
    def +(other: ASCIIArt): ASCIIArt = {
        val art = new ASCIIArt()
        // 獲取最大行數
        val length = if (this.arr.length >= other.arr.length) this.arr.length else other.arr.length
        for (i <- 0 until length) {
            val s = new ArrayBuffer[String]()
            // 獲取this中的行資料, 行數不足,返回空行
            val thisArr: ArrayBuffer[String] = if (i < this.arr.length) this.arr(i) else new ArrayBuffer[String]()
            // 獲取other中的行資料, 行數不足,返回空行
            val otherArr: ArrayBuffer[String] = if (i < other.arr.length) other.arr(i) else new ArrayBuffer[String]()
            // 連線this
            thisArr.foreach(s += _)
            // 連線other
            otherArr.foreach(s += _)
            art.arr += s
        }
        art
    }

    /**
      * 縱向結合
      * @param other
      * @return
      */
    def *(other: ASCIIArt): ASCIIArt = {
        val art = new ASCIIArt()
        this.arr.foreach(art.arr += _)
        other.arr.foreach(art.arr += _)
        art
    }

    override def toString = {
        var ss: String = ""
        arr.foreach(ss += _.mkString(" ") + "\n")
        ss
    }
}

object Six extends App {
    // stripMargin: "|"符號後面保持原樣
    val a = new ASCIIArt(
        """ /\_/\
          |( ' ' )
          |(  -  )
          | | | |
          |(__|__)
          | """.stripMa