Swift中OOP的基本概念以及如何实现它们
对于每种面向对象的语言,三个概念都很突出:封装,继承和多态。 这些概念是编程语言的通用概念,但与面向对象编程紧密相关。 我决定探索Swift / Objective-C中的概念,并在C中实现它们。
某些概念在编程语言中具有不同的形式。 对于每个概念,我都会简要介绍这些概念。
C
如果您是C程序员,则可以跳过此部分。 但是您从来没有碰过C代码或感到有些生锈,这是示例中使用的语言构造的快速入门。
C有两个文件:
- .h —头文件,包含函数和数据声明,由客户端代码包含;
- .c-源代码,实现。
我们将使用指针。 指针是对内存中位置的类型化引用。 指针在类型后使用星号声明。
int *指针;
我们可以创建一个指向内存中任何内容的指针。
C中没有类,但是我们有结构。
struct Foo {
国际会员
};
结构只能包含数据,不能包含功能。 但是我们有指向函数的指针。
struct Foo {
国际会员
无效(*功能)(无效);
};
如果您在Objective-C中使用了块,则看起来很熟悉。
C需要在使用前声明。
struct Foo;
虚函数(void);
为了方便起见,我们可以为类型创建别名。
typedef struct Foo * FooRef; // FooRef现在是Foo的指针
FooRef foo;
我们可以使用点表示法访问结构的成员。 访问指针值称为解引用。 对于结构的成员,我们可以使用->运算符。
*指针= 123;
foo-> member = 123;
在C语言中,我们可以直接分配和取消分配内存,而无需任何内存管理机制。 这是使用malloc和free函数执行的。
FooRef foo =(FooRef)malloc(sizeof(Foo));
free(foo);
通常,C非常专注于处理内存和指针。 通过使用定义良好的内存布局可以实现某些功能。
封装形式
封装是一种限制访问某些数据,变量或字段的机制。 通过限制对数据的访问,我们可以防止意外的更改。 相反,封装公开了要对该数据进行操作的函数。
封装限制了对数据的直接访问,并公开了对这些数据进行操作的功能。
这是一个演示该概念的示例。 假设我正在使用绘图应用程序,并且我的绘图由Canvas类表示。 它包含渲染所需的特定信息(例如大小,像素密度,色彩空间等),无论目的地是显示器,打印机还是位图。
画布类{
///画布的宽度(以像素为单位)
私有(设置)变量宽度:整数
///画布的高度(以像素为单位)
私有(设置)var高度:Int
在里面() {
宽度= 320
高度= 240
}
}
Swift通过属性封装数据。 在Objective-C中,属性是实例变量和综合访问器方法的组合。
@interface画布:NSObject
@property(非原子,只读)int宽度;
@property(非原子,只读)int高度;
@结束
在C语言中,我们对头文件中的数据结构和函数使用前向声明,并在.c文件中实现它们。
// Canvas.h
typedef struct Canvas * CanvasRef;
extern int CanvasGetWidth(CanvasRef canvas);
extern int CanvasGetHeight(CanvasRef canvas);
extern CanvasRef CanvasCreate(void);
// Canvas.c
typedef struct Canvas {
整数宽度
整数高度
}画布;
int CanvasGetWidth(CanvasRef canvas){
返回canvas-> width;
}
int CanvasGetHeight(CanvasRef canvas){
返回canvas-> height;
}
CanvasRef CanvasCreate(void){
画布*画布=(画布*)malloc(sizeof(画布));
canvas-> width = 320;
canvas-> height = 240;
返回画布;
};
所有这三种语言都提供封装机制。 自然,Objective-C中的封装与C非常相似。不同之处在于,编译器将在编译时为属性合成访问器方法。
Swift和Objective-C中的封装机制比C弱。这是任何面向对象语言的普遍问题。 使对象变得更弱的是对象之间的必要关系。 有时我们需要继承的对象有权访问父数据。 这导致访问修饰符的复杂混合。
对于Objective-C,最令人担忧的是语言的动态性质,部分由Swift继承。 例如,键值编码无需任何编译器保护就可以打开封装的数据以进行意外访问。
用C封装是最严格的。 一切都隐藏在.c文件中,无法从外部访问封装的数据。
遗产
继承允许基于现有的继承属性和行为创建新对象。
Swift和Objective-C提供了继承机制。 通常,我们使用继承来扩展特定情况下的现有类型,专门用于更通用的类型。 在示例中, DisplayCanvas扩展了Canvas以提供颜色配置文件信息。
///渲染目标为显示时的画布
类DisplayCanvas:画布{
///显示器的颜色配置文件
列举ColorSpace {
///标准RGB颜色空间
案例sRGB
///宽色域RGB(DCI-P3)色彩空间
案例宽度
}
private(set)var colorSpace:ColorSpace
覆盖init(){
colorSpace = .wideGamutRGB
super.init()
}
}
在C语言中,我们可以使用组合来模拟继承。
// Canvas.h
typedef枚举ColorSpace {
sRGB = 0,
wideGamutRGB
} 色彩空间;
extern CanvasRef DisplayCanvasCreate(void);
// Canvas.m
typedef struct DisplayCanvas {
帆布超级;
ColorSpace colorSpace;
} DisplayCanvas;
CanvasRef DisplayCanvasCreate(void){
DisplayCanvas *画布
=(DisplayCanvas *)malloc(sizeof(DisplayCanvas));
canvas-> super.width = 320;
canvas-> super.height = 240;
canvas-> colorSpace = wideGamutRGB;
return(CanvasRef)canvas;
};
Swift和Objective-C中的继承不仅允许使用现有对象创建新对象,而且还是子类型化机制。 子类型化允许在可以使用基本类型的地方使用继承的类型,代替基本类型。 1994年,计算机科学家Barbara Liskov和Jeannette Wing提出了Liskov替代原理:
子类型要求:令ϕ(x)是有关类型T的对象x的一个可证明性质。然后,对于类型S的对象y,ϕ(y)应为true,其中S是T的子类型。
芭芭拉·利斯科夫(Barbara Liskov)和珍妮特·温(Jeannette Wing)
继承允许基于现有的继承属性和行为创建新对象。 我们必须能够使用可以使用基础对象的新对象。
对于我们的示例,我们必须能够在可以使用Canvas的任何地方使用DisplayCanvas 。
///将画布绘制到目的地
func draw(_ canvas:Canvas){
print(“ width:\(canvas.width),height:\(canvas.height)”)
}
让canvas = DisplayCanvas()
画(帆布)
宽度:320,高度:240
同样适用于C语言。
// Canvas.h
extern void CanvasDraw(CanvasRef canvas);
// Canvas.m
void CanvasDraw(CanvasRef canvas){
printf(“宽度:%d,高度:%d \ n”,
canvas-> width,canvas-> height);
}
// main.c
CanvasRef canvas = DisplayCanvasCreate();
CanvasDraw(画布);
宽度:320,高度:240
由于C对齐内存中结构的方式,如果第一个元素相同,则可以将指针从一个指针转换为另一个指针。 如果我们要更改结构中成员的顺序,则转换将不起作用。
typedef struct DisplayCanvas {
ColorSpace colorSpace;
帆布超级;
} DisplayCanvas;
//我们无法安全地将DisplayCanvas强制转换为Canvas
继承的另一种方法是在基本结构中使用联合和枚举来标识类型。
typedef struct DisplayCanvas {
ColorSpace colorSpace;
} DisplayCanvas;
typedef struct PrintCanvas {
int dpi;
} PrintCanvas;
typedef枚举CanvasType {
显示= 0,
打印
} CanvasType;
typedef struct Canvas {
工会{
DisplayCanvas显示;
PrintCanvas打印机;
超级
CanvasType类型;
}画布;
这样我们可以模拟type(of 🙂和isMemberOfClass:检查。
多态性
多态性允许使用统一接口处理不同的数据类型。
多态性允许使用统一接口处理不同数据类型的值。
我们已经在处理不同数据类型的绘图函数中使用了多态。 记住Liskov替换原则-只要可以使用基本类型,我们就可以使用继承类型。
更有趣的是对象的多态行为。 在Swift和Objective-C中,我们可以使绘图例程成为一种方法,并将其覆盖以实现特定的绘图。
画布类{
func draw(){
打印(“宽度:\(宽度),高度:\(高度)”)
}
}
类DisplayCanvas:画布{
覆盖func draw(){
打印(“宽度:\(宽度),高度:\(高度),颜色空间:\(colorSpace)”)
}
}
让画布:画布= DisplayCanvas()
canvas.draw()
宽度:320,高度:240,色彩空间:wideGamutRGB
为了在C语言中复制此代码,我们可以使用称为动态调度的方法。 您可能已经很熟悉术语,因为Swift和Objective-C实现了它。
动态调度是选择要在运行时调用的多态操作(方法或函数)的实现的过程。
维基百科
我们可以使用函数指针实现动态调度:
// Canvas.c
typedef struct Canvas {
整数宽度
整数高度
//绘制函数的指针
void(* draw)(结构Canvas *);
}画布;
// Draw函数在.h中公开,并包装函数调用
void CanvasDraw(CanvasRef canvas){
canvas-> draw(canvas);
}
//绘图的基本实现
无效_CanvasDraw(struct Canvas * canvas){
printf(“宽度:%d,高度:%d \ n”,
canvas-> width,canvas-> height);
}
//覆盖实现
void _DisplayCanvasDraw(struct DisplayCanvas * canvas){
printf(“宽度:%d,高度:%d,颜色空间:%d \ n”,
canvas-> super.width,canvas-> super.height,
canvas-> colorSpace);
}
CanvasRef DisplayCanvasCreate(void){
DisplayCanvas *画布
=(DisplayCanvas *)malloc(sizeof(DisplayCanvas));
//创建画布时,我们提供函数实现
canvas-> super.draw
=(void(*)(struct Canvas *))&_DisplayCanvasDraw;
// ...
return(CanvasRef)canvas;
};
// main.c
CanvasRef canvas = DisplayCanvasCreate();
CanvasDraw(画布);
宽度:320,高度:240,颜色空间:1
这是非常快速的实现,因为我们可以直接访问功能指针。 但是指针占用空间,并且随着函数数量的增加,我们的结构将占用更多的内存。
最常见的解决方案是使用特殊的数据结构虚拟表存储指向函数的指针。 Objective-C(调度表)和Swift(见证表)都使用这种方法。
// Canvas.c
typedef struct CanvasVTable {
void(* draw)(结构Canvas * canvas);
} CanvasVTable;
typedef struct DisplayCanvasVTable {
void(* draw)(结构DisplayCanvas * canvas);
} DisplayCanvasVTable;
const CanvasVTable kCanvasVTable = {
_画布绘制
};
const DisplayCanvasVTable kDisplayCanvasVTable = {
_DisplayCanvasDraw
};
我们定义两个虚拟表,并为基本和继承结构定义。 我们需要用指向虚拟表的指针替换指向函数的指针。
typedef struct Canvas {
整数宽度
整数高度
const CanvasVTable * vtable;
}画布;
CanvasRef DisplayCanvasCreate(void){
// ...
canvas-> super.vtable =(CanvasVTable *)&kDisplayCanvasVTable;
// ...
return(CanvasRef)canvas;
};
void CanvasDraw(CanvasRef canvas){
canvas-> vtable-> draw(canvas);
}
另外,我们可以在每个结构中添加单独的vtable以便执行覆盖的功能。
Swift和Objective-C通过遍历类层次结构并在见证/调度表中执行查找来实现动态方法解析。
由于正确的封装,我在Canvas.c中所做的所有更改-我从不需要修改客户端代码。