Translate-CS 板


LINE

译自 http://docs.scala-lang.org/tutorials/scala-for-java-programmers.html 因为原文是用markdown撰写,译文也直接用markdown格式撰写 github好读版(?) https://github.com/chikei/scala.github.com/blob/zh_TW/zh/tutorials/ scala-for-java-programmers.md ~~~正文分隔线~~~ ## 介绍 此教学将对Scala语言以及编译器做一个简易的介绍。设定的读者为具有程设经验且想 要看Scala功能概要的人。内文假设读者有着基本、特别是Java上的物件导向程设知识。 ## 第一个例子 这边用标准的 *Hello world* 程式作为第一个例子。虽然它很无趣,可是这让我们在 仅用少量语言下演示Scala工具。程式如下: object HelloWorld { def main(args: Array[String]) { println("Hello, world!") } } Java程式员应该对这个程式的结构感到熟悉:有着一个 `main` 函式,该函式接受一 个字串阵列引数,也就是命令列引数;函式内容为呼叫已定义好的函式 `println` 并 用Hello world字串当引数。 `main` 函式没有回传值(它是程序函式)。因此并不需要 宣告回传型别。 Java程式员不太熟悉的是包着 `main` 函式的 `object` 宣告。这种宣告引入我们一 般称之 *Singleton* 的东西,也就是只有一个实体的类别。所以上面的宣告同时宣告 了一个 `HelloWorld` 类别跟一个这类别的实体,也叫做 `HelloWorld`。该实体会在 第一次被使用到的时候即时产生。 眼尖的读者可能已经注意到这边 `main` 函式的宣告没有带着 `static`。这是因为 Scala没有静态成员(函式或资料栏)。Scala程式员将这成员宣告在单实例物件中,而 不是定义静态成员。 ### 编译这例子 我们用Scala编译器 `scalac`来编译这个例子。`scalac` 就像大多数的编译器一样, 它接受原码档当引数,并接受额外的选项,然後产生一个或多个物件档。它产出的物 件档为标准的Java class档案。 如果我们将上面的程式存成 `HelloWorld.scala` 档,编译的指令为( `>` 是提示字 元,不用打): > scalac HelloWorld.scala 这会在现在的目录产生一些class档案。其中一个会叫做 `HelloWorld.class`,里面 包含着可被 `scala` 直接执行的类别。 ### 执行范例 一旦编译过後,Scala程式可以用 `scala` 指令执行。它的使用方式非常的像执行 Java程式的 `java` 指令,并且接受同样的选项。上面的范例可以用以下的指令来执 行并得到我们预期的输出: > scala -classpath . HelloWorld Hello, world! ## 与Java互动 Scala的优点之一是它非常的容易跟Java程式码沟通。预设汇入所有 `java.lang` 底 下之类别,其他类别则需要明确汇入。 让我们看个展示这点的范例。取得现在的日期并根据某个特定的国家排版成该国的格 式,如法国。 Java的标准函式库定义了一些有用的工具类别,如 `Date` 跟 `DateFormat`。因为 Scala可以无缝的跟Java互动,这边不需要以Scala实作同样的类别--我们只需要汇入 对应的Java套件: import java.util.{Date, Locale} import java.text.DateFormat import java.text.DateFormat._ object FrenchDate { def main(args: Array[String]) { val now = new Date val df = getDateInstance(LONG, Locale.FRANCE) println(df format now) } } Scala的汇入陈述式跟Java的非常像,但更为强大。如第一行,同一个package下的多 个类别可以用大括号括起来一起导入。另外一个差别是,当要汇入套件或类别下所有 的名称时,用下标(`_`)而不是星号(`*`)。这是因为星号是一个合法的Scala识别符号 (如函式名称)。 所以第三行的陈述式导入所有 `DateFormat` 类别的成员。这让静态函式 `getDateInstance` 跟静态资料栏 `LONG` 可直接被使用。 在 `main` 函式中我们先创造一个Java的 `Date` 类别实体,该实体预设拥有现在的 日期。接下来用 `getDateInstance` 函式定义日期格式。最後根据地区化的 `DateFormat` 实体对现在日期排版格式化并印出。最後一行展现了一个Scala有趣的 特点。只需要一个引数的函式可以用中缀语法呼叫。就是说,这个表示式 df format now 是比较不详细版本的这个表示式 df.format(now) 这点也许看起来只是一个小小的语法细节,但是他有着重要的後果,其中一个将会在 下一节做介绍。 让我们以,Scala可以直接继承Java类别跟实作Java介面,来为这节做结尾。 ## 万物皆物件 Scala是一个纯粹的物件导向语言,这句话的意思是说,*所有东西*都是物件,包括数 字、函式。因为Java将基本型别跟参照型别分开,而且没有办法像操作变数一样操作 函式,从这角度来看Scala跟Java是不同的。 ### 数字是物件 因为数字是物件,他们也有函式。事实上,一个像底下的算数表示式: 1 + 2 * 3 / x 只有使用函式呼叫,因为像前一节一样,该式等价於 (1).+(((2).*(3))./(x)) 这也表示着 `+`、`*` 之类的在Scala里是合法的识别符号。 因为Scala的词法分析器对於符号采用最长匹配,在第二版的表示式当中,那些括号是 必要的。也就是说分析器会把这个表示式: 1.+(2) 拆成 `1.`、`+`、`2` 这三个符号。会这样拆分是因为 `1.` 既是合法匹配同时又比 `1` 长。 `1.` 会被解释成文字 `1.0`,使得他被视为 `Double` 而不是 `Int`。把 表示式写成: (1).+(2) 可以避免 `1` 被解释成 `Double`。 ### 函式是物件 可能令Java程式员更为惊讶的会是,Scala中函式也是物件。因此,将函式当做引数传 递、把它们存入变数、从其他函式返回函式都是可能的。能够像操作变数一样的操作 函式这点是*函数编程*这一非常有趣的程设典范的基石之一。 为何把函式当做变数一样的操作会很有用呢,让我们考虑一个定时函式,它的功能是 每秒执行一些动作。我们要怎麽将这动作传给它?最直接的便是将这动作视为函式传 入。应该有不少的程式员对这种简单传递函式的行为很熟悉:通常在使用者介面相关 的程式上,用以注册一些当事件发生时被呼叫的回呼函式。 在接下来的程式中,定时函式叫做 `oncePerSecond` ,它接受一个回呼函式做参数。 该函式的型别被写作 `() => Unit` ,这个型别便是所有无引数且无返回值的函式的 型别( `Unit` 这个型别就像是C/C++的 `void` )。此程式的主函式只是呼叫定时函式 并带入回呼函式,回呼函式输出一句话到终端上。也就是说这个程式会不断的每秒输 出一次"time flies like an arrow"。 object Timer { def oncePerSecond(callback: () => Unit) { while (true) { callback(); Thread sleep 1000 } } def timeFlies() { println("time flies like an arrow...") } def main(args: Array[String]) { oncePerSecond(timeFlies) } } 值得注意的是,这边输出的时候我们使用Scala的函式 `println`,而不是 `System.out` 里的函式。 #### 匿名函式 这程式还有改进的空间。第一点,函式 `timeFlies` 只是为了能够被传递进 `oncePerSecond` 而定义的。赋予一个只被使用一次的函式名字似乎是没有必要的, 最好能够在传入 `oncePerSecond` 时构造出这个函式。Scala可以藉由*匿名函式*来 达到这点。利用匿名函式的改进版本程式如下: object TimerAnonymous { def oncePerSecond(callback: () => Unit) { while (true) { callback(); Thread sleep 1000 } } def main(args: Array[String]) { oncePerSecond(() => println("time flies like an arrow...")) } } 这例子中的右箭头 `=>` 告诉我们有一个匿名函式,右箭头将函式引数跟函式内容分 开。这个例子中,在箭头左边那组空的括号告诉我们引数列是空的。函式内容则是跟 先前的 `timeFlies` 里一样。 ## 类别 之前已讲过,Scala是一个物件导向语言,因此它有着类别的概念。(更精确的说,的 确有一些物件导向语言没有类别的概念,但是Scala不是这类)。Scala宣告类别的语法 跟Java很接近。一个重要的差别是,Scala的类别可以有参数。这边用底下复数的定义 来展示: class Complex(real: Double, imaginary: Double) { def re() = real def im() = imaginary } 这个复数类别接受两个参数,分别为实跟虚部。在创造 `Complex` 的实体时,必须传 入这些参数: `new Complex(1.5, 2.3)`。这个类别有两个函式分别叫做 `re` 跟 `im` 让我们取得这两个部分。 值得注意的是,这两个函式的回传值并没有被明确的给定。编译器将会自动的推断, 它会查看这些函式的右侧并推导出这两个函式都会回传型别为 `Double` 的值。 编译器并不一定每次都能够推断出型别,而且很不幸的是我们并没有简单的规则分辨 哪种情况能推断,哪种情况不能。因为当编译器无法推断未明确给定的型别时它会回 报错误,实务上这通常不是问题。Scala的初学者在遇到那些看起来很简单就能推导出 型别的情况时,应该尝试着忽略型别宣告并看看编译器是不是也觉得可以推断。多尝 试几次之後程式员应该能够体会到何时忽略型别、何时该明确指定。 ### 无引数函式 函式 `re`、`im` 有个小问题,为了呼叫函式,我们必须在函式名称後面加上一对空 括号,如这个例子: object ComplexNumbers { def main(args: Array[String]) { val c = new Complex(1.2, 3.4) println("imaginary part: " + c.im()) } } 最好能够在不需要加括号的情况下取得实虚部,这样便像是在取资料栏。Scala完全可 以做到这件事,需要的只是在定义函式的时候*不要定义引数*。这种函式跟零引数函 式是不一样的,不论是定义或是呼叫,它们都没有括号跟在名字後面。我们的 `Complex` 可以改写成: class Complex(real: Double, imaginary: Double) { def re = real def im = imaginary } ### 继承与覆写 Scala中所有的类别都继承自一个母类别。像前一节的 `Complex` 这种没有指定的例 子,Scala会暗中使用 `scala.AnyRef`。 Scala中可以覆写继承自母类别的函式。但是为了避免意外的覆写,必须加上 `override` 修饰字来明确表示要覆写函式。我们以覆写 `Complex` 类别中来自 `Object` 的 `toString` 作为范例。 class Complex(real: Double, imaginary: Double) { def re = real def im = imaginary override def toString() = "" + re + (if (im < 0) "" else "+") + im + "i" } ## Case Class跟模式匹配(pattern matching) 树是常见的资料结构。如:解译器跟编译器内部常见的表示程式方式便是树;XML文件 是树;还有一些容器是根基於树,如红黑树。 接下来我们会藉由一个小型计算机程式来看看Scala是如何呈现并操作树。这个程式的 功能将会是足以操作简单、仅含有整数、常数、变数跟加法的算术式。`1+2` 跟 `(x+x)+(7+y)`为两个例子。 我们得先决定这种表示式的表示法。最自然表示法便是树,其中节点是操作、叶点是 值。 Java中我们会将这个树用一个抽象母类别表示,然後每种节点跟叶点分别有各自的实 际类别。在函数边程里会用代数资料类型。Scala则是提供了介於两者之间的 *case class*。将它运用在这边会是如下: abstract class Tree case class Sum(l: Tree, r: Tree) extends Tree case class Var(n: String) extends Tree case class Const(v: Int) extends Tree `Sum`、`Var`、`Const` 类别定义成case class代表着它们跟一般类别有所差别: - 在创建类别实体时不需要用 `new`(也就是说我们可以写 `Const(5)`,而不是 `new Const(5)`)。 - 对应所有的建构式参数,Scala会自动定义对应的取值函式(即,对於 `Const` 类别 的实体,我们可以直接用 `c.v` 来取得建构式中的 `v` 参数)。 - `equals` 跟 `hashCode` 会有预设的定义。该定义会根据实体的*结构*而不是个别 实体的识别来运作。 - `toString` 会有预设的定义。会印出"原始型态"(即,`x+1` 的树会被印成 `Sum(Var(x),Const(1))`)。 - 这些类别的实体可以藉由*模式匹配*来拆解。 现在我们有了算术表示式的资料型别,可以开始定义各种运算。我们将从一个可以在 *环境*内对运算式求值的函式起头。环境的用处是赋值给变数。举例来说,运算式 `x+1` 在一个将 `x` 赋与 `5` 的环境(写作 `{ x -> 5 }` )下求值会得到 `6`。 因此我们需要一个表示环境的方法。当然我们可以用一些像是杂凑表的关连性资料结 构,但是我们也可以直接用函式!环境就只是一个将值对应到(变数)名称的函式。之 前提到的环境 `{ x -> 5 }` 在Scala中可以简单的写作: { case "x" => 5 } 这串符号定义了一个当输入是字串 `"x"` 的时候回传整数 `5`,其他输入则是用例外 表示失败的函式。 开始实作之前,让我们先给环境型别一个名字。当然,我们可以直接用 `String => Int`,但是给这型别名字可以让我们简化程式,而且在未来要改动的时 候较为简便。在Scala我们是这样表示这件事: type Environment = String => Int 於是型别 `Environment` 便可以当做输入 `String` 回传 `Int` 函式的型别的代名。 现在我们可以给出求值函式的实作。概念上非常的简单:两个表示式和的值是两个表 示式值的和;变数的值直接从环境取值;常数的值就是常数本身。表示这些在Scala里 并不困难: def eval(t: Tree, env: Environment): Int = t match { case Sum(l, r) => eval(l, env) + eval(r, env) case Var(n) => env(n) case Const(v) => v } 这个求值函式藉由对树 `t` 做*模式匹配*来求值。上述实作的意思应该从直观上便很 明确: 1. 首先检查树 `t` 是否为 `Sum`,如果是的话将左/右侧子树绑定到新变数 `l`/`r` ,然後再对箭头後方的表示式求值;这一个表示式可以使用(而且这边也用到)根据 箭头左侧模式所绑定的变数,也就是 `l` 跟 `r`, 2. 如果第一个检查失败,也就是说树不是 `Sum`,接下来检查 `t` 是否为 `Var`, 如果是的话将 `Var` 所带的名称绑定到变数 `n` 并求值右侧的表示式, 3. 如果第二个检查也失败,表示树不是 `Sum` 也不是 `Var`,那便检查是不是 `Const`,如果是的话将 `Const` 所带的名称绑定到变数 `v` 并求值右侧的表 示式, 4. 最後,如果全部的检查都失败,会丢出例外表示匹配失败;这只会在有更多 `Tree` 的子类别的情况下发生。 如上,模式匹配基本上就是尝试将一个值对一系列的模式做匹配,并在一个模式成功 的匹配时抽取并命名该值的各部分,最後对一些程式码求值,而这些程式码通常会利 用被命名到的部位。 一个经验丰富的物件导向程式员也许会疑惑为何我们不将 `eval` 定义成 `Tree` 类 别跟子类的*函式*。由於Scala允许在case class中跟一般的类别一样定义函式,事实 上我们可以这样做。要用模式匹配或是函式只是品味的问题,但是这会对扩充性有重 要的影响。 - 当使用函式的时候,只要定义新的 `Tree` 子类便新增新的节点,相当的容易。另 一方面,增加新的操作需要修改所有的子类,很麻烦。 - 当使用模式匹配的时候情况则反过来:增加新节点需要修改所有对树做模式匹配的 函式将新节点纳入考虑;增加新的操作则很简单,定义新的函式就好。 让我们定义新的操作以更进一步的探讨模式匹配:对符号求导数。读者们可能还记得 这个操作的规则: 1. 和的导数是导数的和 2. 如果是对变数 `v` 取导数,变数 `v` 的导数是1,不然就是0 3. 常数的导数是0 这些规则几乎可以从字面上直接翻成Scala程式码: def derive(t: Tree, v: String): Tree = t match { case Sum(l, r) => Sum(derive(l, v), derive(r, v)) case Var(n) if (v == n) => Const(1) case _ => Const(0) } 这个函式引入两个关於模式匹配的新观念。首先,变数的 `case` 运算式有一个 *看守*,也就是 `if` 关键字之後的表示式。除非表示式求值为真,不然这个看守会 让匹配直接失败。在这边是用来确定我们只在取导数变数跟被取导数变数名称相同时 才回传常数 `1`。第二个新特徵是可以匹配任何值的*万用字元* `_`。 我们还没有探讨完模式匹配的全部功能,不过为了让这份文件保持简短,先就此打住 。我们还是希望能看到这两个函式在真正的范例如何作用。因此让我们写一个简单的 `main` 函数,对表示式 `(x+x)+(7+y)` 做一些操作:先在环境 `{ x -> 5, y -> 7 }` 下计算结果,然後在对 `x` 接着对 `y` 取导数。 def main(args: Array[String]) { val exp: Tree = Sum(Sum(Var("x"),Var("x")),Sum(Const(7),Var("y"))) val env: Environment = { case "x" => 5 case "y" => 7 } println("Expression: " + exp) println("Evaluation with x=5, y=7: " + eval(exp, env)) println("Derivative relative to x:\n " + derive(exp, "x")) println("Derivative relative to y:\n " + derive(exp, "y")) } 执行这程式,得到预期的输出: Expression: Sum(Sum(Var(x),Var(x)),Sum(Const(7),Var(y))) Evaluation with x=5, y=7: 24 Derivative relative to x: Sum(Sum(Const(1),Const(1)),Sum(Const(0),Const(0))) Derivative relative to y: Sum(Sum(Const(0),Const(0)),Sum(Const(0),Const(1))) 研究这输出我们可以发现,取导数的结果应该在输出前更进一步的化简。用模式匹配 实作一个基本的化简函数是一个很有趣(但是意外的棘手)的问题,在这边留给读者当 练习。 ## 特质(Traits) 除了由母类别继承行为以外,Scala类别还可以从一或多个*特质*导入。 对一个Java程式员最简单去理解特质的方式应该是视他们为带有实作的介面。在Scala 里,当一个类别继承特质时,他实作了该特质的介面并继承所有特质带有的功能。 为了理解特质的用处,让我们看一个经典范例:有序物件。大部分的情况下,一个类 别所产生出来的物件之间可以互相比较大小是很有用的,如排序他们。在Java里可比 较大小的物件实作 `Comparable` 介面。在Scala中藉由定义等价於 `Comparable` 的特质 `Ord`,我们可以做的比Java稍微好一点。 当在比较物件的大小时,有六个有用且不同的谓词(predicate):小於、小於等於、等 於、不等於、大於等於、大於。但是把六个全部都实作很烦,尤其是当其中有四个可 以用剩下两个表示的时候。也就是说,(举例来说)只要有等於跟小於谓词,我们就可 以表示其他四个。在Scala中这些观察可以很漂亮的用下面的特质宣告呈现: trait Ord { def < (that: Any): Boolean def <=(that: Any): Boolean = (this < that) || (this == that) def > (that: Any): Boolean = !(this <= that) def >=(that: Any): Boolean = !(this < that) } 这份定义同时创造了一个叫做 `Ord` 的新型别,跟Java的 `Comparable` 介面有着同 样的定位,且给了一份以第一个抽象谓词表示剩下三个谓词的预设实作。因为所有的 物件预设都有一份等於跟不等於的谓词,这边便没有定义。 上面使用了一个 `Any` 型别,在Scalla中这个型别是所有其他型别的母型别。因为它 同时也是基本型别如 `Int`、`Float`的母型别,可以将其视为更为一般化的Java `Object` 型别。 因此只要定义测试相等性跟劣性的谓词,并且加入 `Ord`,就可以让一个类别的物件 们互相比较大小。让我们实作一个表示阳历日期的 `Date` 类别来做为例子。这种日 期是由日、月、年组成,我们将用整数来表示这三个资料。因此我们可以定义 `Date` 类别为: class Date(y: Int, m: Int, d: Int) extends Ord { def year = y def month = m def day = d override def toString(): String = year + "-" + month + "-" + day 这边要注意的是宣告在类别名称跟参数之後的 `extends Ord`。这个语法宣告了 `Date` 继承 `Ord` 特质。 然後我们重新定义来自 `Object` 的 `equals` 函式好让这个类别可以正确的根据每 个资料栏来比较日期。因为在Java中 `equals` 预设实作是直接比较实际物件本身, 并不能在这边用。於是我们有下面的实作: override def equals(that: Any): Boolean = that.isInstanceOf[Date] && { val o = that.asInstanceOf[Date] o.day == day && o.month == month && o.year == year } 这个函式使用了预定义函式 `isInstanceOf` 跟 `asInstanceOf`。`isInstanceOf` 对应到Java的 `instanceof` 运算子,只在当使用它的物件的型别跟给定型别一样时 传回真。 `asInstanceOf` 对应到Java的转型运算子,如果物件是给定型别的实体, 该物件就会被视为给定型别,不然就会丢出 `ClassCastException` 。 最後我们需要定义测试劣性的谓词如下。 def <(that: Any): Boolean = { if (!that.isInstanceOf[Date]) error("cannot compare " + that + " and a Date") val o = that.asInstanceOf[Date] (year < o.year) || (year == o.year && (month < o.month || (month == o.month && day < o.day))) } 这边使用了另外一个预定义函式 `error`,它会丢出带着给定错误讯息的例外。这便 完成了 `Date` 类别。这个类别的实体可被视为日期或是可比较物件。而且他们通通 都定义了之前所提到的六个比较谓词: `equals`跟`<` 直接出现在类别定义当中,其 他的则是继承自 `Ord` 特质。 特质在其他场合也有用,不过详细的探讨它们的用途并不在本文件目标内。 ## 泛型 在这份教学里,我们最後要探讨的Scala特性是泛型。Java程式员应该相当的清楚在 Java 1.5之前缺乏泛型所导致的问题。 泛型指的是能够将型别也作为程式参数的功能。举例来说,当程式员在为链结串列写 函式库的时候,他必须决定串列的元素型别为何。由於这串列是要在许多不同的场合 使用,不可能决定串列的元素型别为如 `Int` 一类。这样限制太多。 Java程式员采用所有物件的母类别 `Object`。这个解决办法并不理想,一方面这并不 能用在基础型别(`int`、`long`、`float`之类),再来这表示必须靠程式员手动加入 大量的动态转型。 Scala藉由可定义泛型类别(跟函式)来解决这问题。让我们藉由最简单的类别容器来检 视这点:参照,它可以是空的或者指向某型别的物件。 class Reference[T] { private var contents: T = _ def set(value: T) { contents = value } def get: T = contents } 类别 `Reference` 带有一个型别参数 `T`,这个参数会是容器内元素的型别。此型别 被用做 `contents` 变数的型别、 `set` 函式的引数型别、 `get` 函式的回传型别。 上面的程式码使用的Scala的变数语法,应该不需要过多的解释。值得注意的是赋与该 变数的初始值是 `_`,该语法表示预设值。数值型别的预设值是0,`Boolea8n` 型别 是伪, `Unit` 型别是 `()` ,所有的物件型别是 `null`。 为了使用 `Reference` 类型,我们必须指定 `T`,也就是这容器所包容的元素型别。 举例来说,创造并使用该容器来容纳整数,我们可以这样写: object IntegerReference { def main(args: Array[String]) { val cell = new Reference[Int] cell.set(13) println("Reference contains the half of " + (cell.get * 2)) } } 如例子中所展现,并不需要先将 `get` 函式所回传的值转型便能当做整数使用。同时 因为被宣告为储存整数,也不可能存除了整数以外的东西到这一个容器中。 ## 结语 本文件对Scala语言做了快速的概览并呈现一些基本的例子。对Scala有更多兴趣的读 者可以阅读有更多进阶范例的 *Scala By Example*,并在需要的时候参阅 *Scala Language Specification*。 --



※ 发信站: 批踢踢实业坊(ptt.cc)
◆ From: 211.72.92.133







like.gif 您可能会有兴趣的文章
icon.png[问题/行为] 猫晚上进房间会不会有憋尿问题
icon.pngRe: [闲聊] 选了错误的女孩成为魔法少女 XDDDDDDDDDD
icon.png[正妹] 瑞典 一张
icon.png[心得] EMS高领长版毛衣.墨小楼MC1002
icon.png[分享] 丹龙隔热纸GE55+33+22
icon.png[问题] 清洗洗衣机
icon.png[寻物] 窗台下的空间
icon.png[闲聊] 双极の女神1 木魔爵
icon.png[售车] 新竹 1997 march 1297cc 白色 四门
icon.png[讨论] 能从照片感受到摄影者心情吗
icon.png[狂贺] 贺贺贺贺 贺!岛村卯月!总选举NO.1
icon.png[难过] 羡慕白皮肤的女生
icon.png阅读文章
icon.png[黑特]
icon.png[问题] SBK S1安装於安全帽位置
icon.png[分享] 旧woo100绝版开箱!!
icon.pngRe: [无言] 关於小包卫生纸
icon.png[开箱] E5-2683V3 RX480Strix 快睿C1 简单测试
icon.png[心得] 苍の海贼龙 地狱 执行者16PT
icon.png[售车] 1999年Virage iO 1.8EXi
icon.png[心得] 挑战33 LV10 狮子座pt solo
icon.png[闲聊] 手把手教你不被桶之新手主购教学
icon.png[分享] Civic Type R 量产版官方照无预警流出
icon.png[售车] Golf 4 2.0 银色 自排
icon.png[出售] Graco提篮汽座(有底座)2000元诚可议
icon.png[问题] 请问补牙材质掉了还能再补吗?(台中半年内
icon.png[问题] 44th 单曲 生写竟然都给重复的啊啊!
icon.png[心得] 华南红卡/icash 核卡
icon.png[问题] 拔牙矫正这样正常吗
icon.png[赠送] 老莫高业 初业 102年版
icon.png[情报] 三大行动支付 本季掀战火
icon.png[宝宝] 博客来Amos水蜡笔5/1特价五折
icon.pngRe: [心得] 新鲜人一些面试分享
icon.png[心得] 苍の海贼龙 地狱 麒麟25PT
icon.pngRe: [闲聊] (君の名は。雷慎入) 君名二创漫画翻译
icon.pngRe: [闲聊] OGN中场影片:失踪人口局 (英文字幕)
icon.png[问题] 台湾大哥大4G讯号差
icon.png[出售] [全国]全新千寻侘草LED灯, 水草

请输入看板名称,例如:Soft_Job站内搜寻

TOP