8.3 保护字段和封装:
TNewDate
类中 GetText 方法的代码只有在与 TDate 类相同的单元中编写时才能编译。事实上,这个方法访问的是父类的 FDate 的私有字段。如果我们想将子类放在一个新的单元中,就必须将 FDate 字段声明为protected(或者strict protected),或者在父类中添加一个简单的protected方法来读取私有字段的值。
一些开发人员认为,第一种解决方案是最好的,因为将大部分字段声明为protected会使类的可扩展性更强,也更容易编写子类。然而,这违反了封装的理念。在一个庞大的类层次结构中,更改基类中某些protected字段的定义就像改变某些全局数据结构一样困难。如果有十个派生类访问这些数据,那么改变其定义就意味着可能要修改这十个类中的代码。
换句话说,灵活性、扩展性和封装性往往成为相互冲突的目标。当这种情况发生时,你应该尽量倾向于封装。如果能在不牺牲灵活性的前提下做到这一点,那就更好了。通常,这种中间解决方案可以通过使用虚方法来实现,我将在下文 "后期绑定和多态性 "一节中详细讨论这个话题。如果为了加快子类的编码速度而不使用封装,那么你的设计可能就没有遵循面向对象的原则。
请记住,protected字段与private字段具有相同的访问规则,因此同一单元中的任何其他类都可以访问其他类的protected成员。如前一章所述,通过使用strict protected访问指定符,可以实现更强的封装。
8.3.1 使用"Protected"黑客技术
如果你是 Object Pascal 和 OOP 的新手,这将是一个相当高级的章节。第一次阅读这本书时,你可能想跳过这部分内容,因为它可能会让你感到相当困惑。
鉴于单元保护的工作原理,除非使用strict protected 关键字,否则即使是当前单元中声明的类的基类的受保护成员也可以被直接访问。这就是通常所说的 "protected hack "背后的原理,即定义一个与其基类完全相同的派生类的唯一目的就是访问基类的受保护成员。下面是它的工作原理。
我们已经看到,一个类的私有数据和受保护数据可以被与该类出现在同一单元中的任何函数或方法访问。例如,请看这个简单的类(保护示例的一部分):
type
TTest = class
protected
FProtectedData: Integer;
public
PublicData: Integer;
function GetValue: string;
end;
GetValue方法只是返回包含两个整数值的字符串:
function TTest.GetValue: string;
begin
Result := Format('Public: %d, Protected: %d', [PublicData, FProtectedData]);
end;
一旦将这个类放在自己的单元中,您将无法直接从其他单元访问其受保护的部分。因此,如果您编写以下代码,
procedure TForm1.Button1Click(Sender: TObject);
var
Obj: TTest;
begin
Obj := TTest.Create;
Obj.PublicData := 10;
Obj.FProtectedData := 20; // 编译不通过
Show(Obj.GetValue);
Obj.Free;
end;
编译器将报出错误消息“Undeclared identifier: ‘FProtectedData’”。此时,您可能认为没有办法直接访问在不同单元中定义的类的protected数据。然而,还是有办法的。
考虑一下如果创建一个明显无用的派生类会发生什么,例如:
type
TTestAccess = class(TTest);
现在,在声明它的同一单元中,您可以调用 TTestAccess 类的任何protected方法。事实上,您可以调用在同一单元中声明的类的protected方法。
这对使用 TTest 类对象有什么帮助呢?考虑到这两个类共享完全相同的内存布局(因为没有任何区别),您可以强制编译器将一个类的对象当作另一个类的对象来处理,这通常是一种不安全的类型转换:
procedure TForm1.Button2Click(Sender: TObject);
var
Obj: TTest;
begin
Obj := TTest.Create;
Obj.PublicData := 10;
TTestAccess(Obj).FProtectedData := 20; // 编译通过!
Show(Obj.GetValue);
Obj.Free;
这段代码编译并正常工作,正如您通过运行Protection示例所看到的。同样,原因是 TTestAccess 类自动继承了 TTest 基类的受保护字段,而且由于 TTestAccess 类与试图访问继承字段中数据的代码位于同一单元,因此受保护的数据是可访问的。
既然我已经向你演示了如何做到这一点,我必须警告你,以这种方式违反类保护机制很可能会导致程序出错(因为你访问了确实不应该访问的数据),而且它与良好的 OOP 方法论背道而驰。不过,在极少数情况下,使用这种技术是最好的解决方案,这一点你可以通过查看库源代码和许多组件的代码来了解。
总的来说,这种技术是一种 "黑客 "行为,应尽可能避免使用,尽管它可以被视为语言规范的一部分,并适用于所有平台以及所有现在和过去的 Object Pascal 版本。