Swift 十二讲 第六章 类 (Class) (draft)
1.类的简单介绍
- Swift的类的定义简单便捷,但足够丰富。
简单的说,类就是一个封装变量和函数的容器的定义。变量被称为类的属性。函数一般是操作这些变量的,被称为方法。类可以从其他的类继承而来,然后添加一些新的属性和方法。如果一个类不从任何其他的类继承而来,它被称为基类。
-
类是一个各种定义的集合。Swift的类不需要声明,直接定义就可以。不像C++那样声明和定义分两步才能写完一个类。当类完成定义后,你可以把它想象为一个菜谱或者像整型那样的类型。这时它还没有实例。然后你需要创建一个实例。实例创建后,你就可以使用其内部的属性和方法。当被创建之后,实例包括的属性,方法等等和一般的变量常量函数的区别不大。实例也被叫做对象。这就是面向对象编程这个说法的由来。但作者更喜欢实例这个名字。
-
类的实例是索引传递或者指针传递的。当你创建一个实例后,然后如果没有再次创建另一个实例的话。无论给这个实例多少变量名,永远指的都是这一个实例。如下面例子所示:
class 矩形 {
var 长度 = 1
var 宽度 = 2
var 面积: Int
{
return 长度*宽度
}
} //矩形类被定义出来。
let x1 = 矩形()
x1.长度=20
println(x1.长度) //输出20
var x2 = x1
x2.长度 = 40
println(x1.长度) //输出变为40; 这里x2和x1其实是一个实体的不同名字而已。
//因为类只被创建了一次实例。
- 如上例所示, let x1 = 矩形()的意思是,因为用了let, x1不能再被赋予别的值。你不能再把别的值赋给它。但是,你可以改动x1所指的实例的属性。
x1.长度=20是合法的。在Swift里,不能创建不可改动的实例。
2.属性
-
属性有三种:存储属性和计算属性,这两种是和实例绑定的实例属性。类型属性是和类绑定的类型属性。
-
存储属性
如上例所示,存储属性就是一些在创建时候分配了内存的和实例绑定在一起的变量或者常量。
*计算属性
上面例子里的“面积”是计算属性。它实际上是个函数或者方法,但是写成类似于属性的格式,方便使用。计算属性必须显式定义其类型。计算类型的完整格式如下:
var 计算类型的名字:类型
{
get
{
}
set(值的名字)
{
}
}
如上所示,get是在这个计算属性被读的时候调用的函数。set是当这个计算属性被赋值的时候调用的函数。如下例:
class 矩形 {
var 长度 = 1
var 宽度 = 2
var a = 1
var 面积: Int
{
get{return 长度*宽 }
set(a)
{长度 = a}
}
}
let x1 = 矩形()
x1.面积=20
println(x1.长度) //输出20
println(x1.面积) //输出40, 因为长度已经被set变为20
上面例子的执行过程是这样的。x1.面积=20 这句调用了set,所以x1.长度被变成20。println(x1.面积)调用get计算面积,得到40。
- 属性观测器
属性观测器是两个可以附加在存储属性之后的函数。一个是willSet,另一个是didSet。前者在属性被赋值之前调用。后者在属性被赋值之后调用。
这两个函数的参数就是你要赋的值,其名字你可以自己指定。如果不指定参数名,那么可以在函数体内用newValue代替。如下例:
class 动物 {
var 名字: String = ""
{
willSet
{
println(newValue)
}
}
}
var 我的动物 = 动物()
我的动物.名字 = "小狗"
//因为名字被赋值,你可以看到playground的console有一个输出:”小狗”。
//这是因为println(newValue)被调用了。
这里有几个细节要注意。首先如果你要用willSet和didSet函数,那么必须明确指定属性的类型。第二你也必须初始化。
很明显,willSet和didSet是非常有用的功能。你可以重载IOS内建类的一些属性,添加这两个函数,来完成实例属性变化时候的一些操作。
例如一个按钮颜色变了,可以在willSet里面触发一些你想要的其他操作。
- 类型属性
类型属性是和类绑定的计算属性。Swift目前只支持计算类型属性。不支持存储类型属性。它的写法为:
class SomeClass {
class var someComputedProperty: SomeType {
get { return SomeType } set(valueName)
{
// do something with valueName
// that sets the property }
} }
类型属性是在类上被读写,而不是在实例上。如下例:
class a {
class var A:Int
{
get {return -1}
set
{println("xxx")}
}
}
var x1 = a()
println(a.A) //输出-1
a.A = 5 //输出xxx,因为set被调用了
println(a.A) //输出-1
- 常量属性和变量属性
变量属性在类里面定义的时候,必须赋予初始值。常量属性可以不需要给出初始值,而是用init函数赋值。如下例:
class bx
{
let b: Int
init(c : Int)
{
b = c
}
}
var bb = bx(c : 1) //c的值1,被传递进去init(c:Int)函数
println(bb.b) //输出为1
常量属性不能在子类里面被初始化。常量属性可以赋予初始值,然后在init()里面被改变。但是无论如何,类被实例化的时候,常量属性必须有一个值。不然就不能实例化成功。
3.方法
方法有两种。第一种是类型方法,用class func关键字定义。第二种是实例方法。
- 方法和函数的参数的重要区别。
读者可能还记得。函数的参数标准定义为: (外部名 内部名 :类型)。 如果没有外部名,那么函数的参数默认是局部的。方法于此是不同的,方法的第一个参数默认是局部的。剩余的其他参数,则是默认同时是局部和外部的,如果你不明确定义外部名。第二个参数开始的其他所有参数的外部名,默认等同于内部名。
在方法的定义里,如果你想给第一个参数一个相同的外部名,那么可以用#关键字。如果你不想让第二个开始的其他参数有外部名,那么可以用_ (下划线)关键字。
这段定义非常麻烦。作者认为很大程度上,这是为了方便调用object-c的一些方法。
- 类型方法和实例方法的简单例子
下面例子简单的说明了类型方法的定义和使用。
class bx
{
var ap = 1
class func method1()
{println("type method is defined")}
}
var a = bx()
println(a.ap) //输出为1
bx.method1() //输出为"type method is defined"
下面例子简单说明了实例方法的定义和使用:
class bx
{
var ap = 1
func method2()
{println(" method is defined")}
}
var a = bx()
println(a.ap) //输出1
a.method2() //输出:method is defined
-
下标方法。
Swift里,你可以定义下标方法,这样你的实例引用下标的语句看起来类似于数组。实际上下标方法和一般的方法没什么区别。不过用了关键字subscript之后,会让你对方法的引用看起来像个数组而已。例如:class a { subscript(c:Int)->Int { println(c) return 0 } } var b = a() b[2] //输出为2。这是因为下表函数被调用。 println(b[2]) //输出为2,然后输出0。这是因为下表函数的返回值为0.
值得注意的是,下表函数可以用浮点数做为参数或者自变量。这是和数组不同的地方。
如下例:
class a {
subscript(c:Double)->Int
{
println(c)
return 0
}
}
var b = a()
println(b[3.14]) //输出为3.14, 0。
如上面两个例子所示,下标其实就是看上去写法更像数组的函数。
- 继承和重载要用到的一个简单例子
中文把supercalss和subclass翻译成父类,子类实际上是很扯的事情。本文将把superclass翻译成上类,subclass翻译成下类。强调其几何关系。
我们先定义一个基类,弄清楚其意思,然后再逐步用例子说明如何重载和继承。这个基类如下:
class a {
var A = 1
func B (c:Int) -> Double
{
return (Double(c*c/2))
}
subscript(c:Double)->Int
{
return (Int(c*c))
}
}
var b = a()
println(b[3.14])
println(b.B(3))
这个基类很简单。但包含了属性,方法和下标这三个最常用的类的元素。
- 继承的写法
继承就是根据一个已经有了的类,定义一个新类。在写继承的时候,你只要按如下格式,表明其上类即可。然后你可以在新类里面添加自己需要的新的元素(属性方法等等)。
class 新类的名字: 上类的名字 { }
如下例:
class aa: a{
var x = 20
} //上类a的定义在前面一个例子
var bb = aa()
println(bb[3.14]) //输出为9。调用了上类已经有的下标定义
println(bb.x) //输出20
aa继承了a这个已经有了的类。然后什么也没做。你可以正常使用aa。
- 重载
一般情况下,你继承一个已经有了的上类,自己创建了一个类,一定是为了做什么事。所以肯定会新定义一些东西,然后改变上类里已经有的一些属性方法等等。后者就是重载。
重载的写法很简单,记住下面要点,按照常识写,基本不会出错:
(1)重载的关键字是override。
(2)引用上类里已经有的属性方法和下标的话,记住要加super关键字。
如下例:
class a {
var A = 1
func B (c:Int) -> Double
{
return (Double(c*c/2))
}
subscript(c:Double)->Int
{
return (Int(c*c))
}
}
class aa: a{
var x = 20
override subscript(c:Double)->Int
{
return (super[c]*2)
}
}
var b = a()
var bb = aa()
println(b[2]) //输出4
println(bb[2]) //输出8
在上面例子中,b是类a的一个实例。bb是a的下类aa的实例。a的下标函数被重载了。
- 禁止重载,禁止继承
你可以在一些类的属性方法等前面加上final关键字,禁止其被重载。
在class关键字之前加上final, 可以禁止其被继承。
*Swift不允许多重继承
Swift不允许多重继承。你可以用多重的,各种协议(protocol)。协议会在后面讲到。笔者认为这是个很明智的决定。多重继承带来的混乱远远超过其好处。
- init()和self
有时候你并不希望给类里面的存储属性指定默认初始值。而是希望在创建实例的时候,你把初始值传给它。这时候就要用到init ()函数。
Swift有两种初始化函数。第一种就是init()。第二种是在init()前面加上convenience关键字。我把第二种叫做快捷初始化。
每个类都有个self属性,指的就是被创建的实例。self最常见是在初始化名字冲突时候使用。
下面是一些例子:
class a {
var A,B,C : Int
init()
{
A = 1;
B = 1;
C = 1;
}
}
var b = a()
println(b.A)
//这个例子毫无意义,纯粹为了说明句法。A的初始化完全可以写成var A =1。省事省时间。
再看下面的例子:
class a {
var A,B,C : Int
init(A:Int,C:Int)
{
self.A = A;
B = 1;
self.C = C;
}
}
var b = a(A:1,C:10)
println(b.A) //输出1
println(b.B) //输出1
println(b.C) //输出10
//这个例子,在创建实例的时候才初始化A和C属性。但是为了直观,所以init()里面被传递的参数名字也用了A和C。所以要用self来告诉编译器怎么做。B就不需要加self,自动就是指的类的属性。
有个笑话: "张三不仅仅是个人,还是个名字(也就是类,非实体)。" 可见,你到底指的是 张三的名字还是张三这个人,非得再加一点关键字不可。
名字和其所指是个很严肃的问题,大家可以思考下。
上帝是个名字,还是实体?下面是托马斯阿奎那的十二个问题:
(1)上帝是否能够由我们命名?
(2)任何用于上帝的名称是否都是实体性地言说他的?
(3)任何用于上帝的名称究竟是照字义说到他呢,还是全都是在比喻地使用呢?
(4)用于上帝的许多名称是否是同义的?
(5)用到上帝和受造物身上的一些名称究竟是单义的,还是多义的?
(6)如果它们是类比地应用的话,那么,它们是首先应用到上帝身上呢还是受造物身上呢?
(7)任何名称是否是时间性地应用到上帝身上的?
(8)“上帝”这个名称究竟是一个关于本性的名称呢,还是一个关于运作的名称呢?
(9)“上帝”这个名称是否是一个可以传达的名称?
(10)当藉本性、分有和意见指谓上帝时,这是在单义地还是在多义地使用“上帝”这个名称的?
(11)“自有”这个名称是否是至上地适合于上帝的名称?
(12)能否形成关于上帝的肯定命题?
- 去初始化
你可以在类里面写deinit关键字,表明你在一个实例被清除前你希望做的事。语法如下:
class xxx {
...
deinit {你要做的事}
...
}
如下例:
class a {
var A,B,C : Int
init(A:Int,C:Int)
{
self.A = A;
B = 1;
self.C = C;
}
deinit
{println("Being = Nothing")}
}
class aa:a {}
var b = a(A:1,C:10)
b = aa(A:1,C:10)
//当b换成子类aa的时候,没有实例再实现a了。所以这时候输出"Being = Nothing"
- 类型下降
Swift的类型下降关键字是as。还有一个判断类型的关键字是is, is的操作返回布尔值。as可以把上类指定为下类。(as加问号?会在后面optional里面讲)。
Swift不支持多重继承。所以这个类型下降,按笔者的理解,其实没多大作用。但是这个as,对IOS开发作用重大。因为种种原因,IOS里旧的object-c类库里面很多方法返回的是所谓的Anyobject,也就是任何类型的实例都可以。那么当你创建这些IOS里面的对象的时候,往往需要用as,来确保其正常工作。
如下例:
import UIKit
let button = UIButton.buttonWithType(UIButtonType.System) as UIButton
button.frame = CGRectMake(100, 100, 100, 50)
button.backgroundColor = UIColor.greenColor()
//在playground下面的输出里,可以看到一个绿方块。
读者可以试着去掉"as UIButton",可以看到playground会报错,说需要给any object指定一个更下面的类。另外,你可以把as UIButton换成as UIView,这段代码也可以正常使用。
command+单击buttonWithType可以发现这个方法的定义为:
class func buttonWithType(buttonType: UIButtonType) ->AnyObject
可见其返回值确实是AnyObject。所以必须下降为UIButton才能用其他各种button相关的属性。