上一章:函式 目录 下一章:界面


类别 编辑

类别是物件导向语言的基础。不像其他的 .NET 语言,Boo 在程式里并不一定要有类别。

下面的类别 Person 表示了一个人,而里面有两个字段:_name_age与两个方法:建构子与 ToString。类别可以说是建立新物件的样板,诚如星星形状的模子是制作星星形状饼干的样板一样。回到主题,以 Person 产生出来的物件包含两个资讯,人的名字与年龄。建构子在物件被建构出来时进行初始化字段资料的动作,建构之后,就可以呼叫该物件的方法。

# class1.boo
class Person:
	_name as string
	_age as int
	
	def constructor(name as string, age as int):
		_name = name
		_age = age

	def ToString():
		return _name

p = Person("john",36)
print p.ToString()

输出结果

john

上面的代码就跟下面的 C# 程式一样:

using System;

class Person {
	protected string _name;
	protected int _age;
	
	public Person(string name, int age) {
		_name = name;
		_age = age;
	}

	public string ToString() {
		return _name;
	}
}
	
class TestPerson {
	public static void Main(string[] args) {
		Person p = new Person("john",36);
		Console.WriteLine(p.ToString());		
	}
}

Boo 的版本明显地比较短,而且也不需要大括号 { },比较易于了解。Boo 的字段可视性预设为 protected,方法可视性则预设为 public﹔C# 对可视性则完全预设为 private。最后面的代码都会被放在隐藏模组类别里的 Main 方法,在这里,编译器会将此隐藏模组类别命名为 class1Module。你可以明确地将模组类别写出,让 Boo 看起来更像 C# 的代码:

import System
[module]
class TestMain:
	public static def Main():
		Console.WriteLine("hello, world!")

物件属性 编辑

你可以把物件的属性当作是物件的公开字段。在 Windows form 程式里,我们会常看到大部分控件都有 Text 属性。如果你改变了 form 的 Text 属性,form 的标题就会被改变。怎么做的呢?这是因为属性只是一对函式:getter 用来回传值,而 setter则用来设置内部状态。所以 form.Text="Hello!"实际上是呼叫了 form.set_Text("Hello!")(很明显地跟 Java 相似)。在前面提过的 Person 类别里,我们可以增加一个 Name 的属性,实际上是存取 _name 字段:

	Name:
		get:
			return _name
		set:
			_name = value

Boo 提供了一个快捷的方法,让代码看起来更简洁,新版本的 'Person 如下:

class Person:	
	[property(Name)]
	_name as string

	[property(Age)]
	_age as int

	override def ToString():
		return _name
		
p = Person(Name:"john",Age:36)
print p

注意,我们不再需要建构子来初始这些字段了。有了这些公开的属性,我们可以在呼叫建构子的时候,表明要初始化这些属性,这让初始化物件的代码更清楚(这个方法只能在呼叫建构子时使用)。

Getters与预处理条件 编辑

只读属性是很常见的情况,这个时候只需要定义 getter 函式:

  Name:
    get
      return _name

Boo 提供 getter attribute,让你可以用更简洁的方式来写这段代码:

  [getter(Name)]
  _name as string

一般来说,越早处理不好的输入越好。这会简化后续的处理﹔之后就可以假设资料是正确的,因为有坚固的防火墙在系统与真实世界之间。举例来说,类别:Person 的 Name 应该避免为空白或是 null 字串。 property attribute 提供了一个可选择的引数,你可以指定预处理条件,当符合此条件时,才可以进行 setter 动作,否则就失败。

  [property(Name,value is not null and len(value) > 0)]
  _name = "que?"

这样的代码比下面的代码短多了:

  _name = "que?"
  Name:
    get:
      return _name
    set:
      assert value is not null and len(value) > 0
      _name = value

这样的写法的确节省了不少行数,但实际上编译出来的结果并没有。否则,我们将结束继续作像是在同一行放置数个指派等的笨事( Otherwise, we would end up doing silly things like placing multiple assigments etc on the same line.)。这样写的好处,是让程序员的意图更为清楚。在 自订Attribute 一章里,我将示范一个新的属性:NonEmptyString,这会让代码的意图更为容易被了解,也更容易输入。

为什么需要属性 编辑

存取属性值,其实很像是呼叫函式,所以属性的存取明显地比存取公开字段要慢 (虽然有最佳化)。但是这仍有几个好处,第一,属性对框架来说是公开且可用的。最好的例子是 Windows Forms 里的控件 PropertyGrid

// 譯註原程式無法執行因為少了 Application.Run這裡稍作改寫
import System.Windows.Forms from "System.Windows.Forms"

Application.EnableVisualStyles()
Application.SetCompatibleTextRenderingDefault(false)

f=Form()
f.Text="Test"
pg=PropertyGrid( Dock: DockStyle.Fill, CommandsVisibleIfAvailable: true, Text: "Property Grid Demo" )
f.Controls.Add( pg )
pg.SelectedObject=f
f.Show()

Application.Run( f )

这很酷吧,你可以检阅刚刚建立的 form 的所有属性。

第二,在需要改变属性实作时,比较不会影响到函式库里其他用到此属性的部分。看看这两种方法:

class Bill:
	public DateDue as DateTime
	public Amount as double
	public Debtor as Customer
	...
		
class Bill:
	[property(DateDue)]
	_dateDue as DateTime
	[property(Amount)]
	_amount as double
	[property(Debtor)]
	_debtor as Customer
	...

前者的确可用,但想想,如果你突然想检查这些数值(参考上一节),你就得改变为第二种方式来作。如果 Bill 类别是你系统组件公开界面的一部分,那么你的程式将可能出错,并需要重新编译。如果组件是在使用者的电脑上,那么情况会更险恶,而且也不易递增升级你的系统。

多载与继承 编辑

CLI 架构下,所有类别最终都继承自 Object。所以可以说 Object 定义了所有物件的通用行为。最常被用到的行为,就是将自己转为字串,这通常会被多载﹔他的名字就是 ToString 方法。当框架需要显示物件的文字时,它就会呼叫这个方法 (这就是 print 知道如何显示的原因)。预设是简单地传回类别名称,这很安全,而且很有用(尤其是在互动环境下),但是我们可以多载 ToString 让它传回我们想要的任何事。

举例来说,如果我们像这样定义了 PersonToString

override def ToString():
	return _name

那么物件将知道怎么印出它自己:

p = Person("jane",25)
print p

继承的基本策略就在于利用既有的类别,并且延伸它。举例来说,'Employee 是个 Person (通常是)。它有些行为与 Person 一样,但却有独特的 ID。

class Employee(Person):
	_employeeId as int
	
	def constructor(name as string, age as int, id as int):
		super(name,age)
		_employeeId = id
		
	override def ToString():
		return super.ToString() + " id = " + _employeeId
		
e = Employee("James",47,2333)
print e

输出结果

James id = 2333

请注意关键字 super﹔它用来参照父类别。所以 Employee 建构子得以初始化父类别Person,也可以在方法里使用父类别的某些方法。

事实上,如果建构子没有引数 (这被称为预设建构子)的话,并不一定要呼叫父类别的建构子。在没有建构子的情况下,任何字段将被初始化为 0 或 null,或是任何有意义的初始值。

class Base:
	public X as double
	
	def constructor():
		X = 1.0

class Inherits(Base):
	public Y as double
			
b = Inherits()
b.Y = 2.0
print b.X,b.Y

我们可以说,Inherits物件是一种特别的 Base 物件。

Java 程序员要注意一点,方法预设并非 virtual (虚拟),所以你必须明确指定要多载继承下来的方法。这与 C# 一样﹔了解 C# 的作法,会让你比较容易清楚 Boo 的作法。

命名惯例 编辑

Boo 与大部分的程式语言一样,并没有坚决主张一个命名惯例。在一般用法上,会将字段名称以 _ 开头,而方法的名称以 camel case 命名。此外,你也可以参考其他 .NET 语言或 Python 的作法。


上一章:函式 目录 下一章:界面