快學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 特質的構造順序
和類一樣,特質也可以有構造器,由欄位的初始化和其他特質中的語句構成。
構造器以如下順序執行:
- 首先呼叫超類的構造器。
- 特質構造器在超類構造器之後、類構造器之前執行。
- 特質由左到右被構造。
- 每個特質當中,父特質先被構造。
- 如果多個特質共用一個父特質,而這個父特質已經被構造,則不會再次構造。
- 所有特質構造完畢,子類被構造。
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