★Dart-7-扩展类和接口
Posted itzyjr
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了★Dart-7-扩展类和接口相关的知识,希望对你有一定的参考价值。
1.用继承扩展类
class User {
User(String this._username) {}
String _username;// private
String _existingPasswordHash;// private
String get username => _username;
String emailAddress;
bool isPasswordValid(String newPassword) {
//... some validation code ...
}
}
您的企业服务需要在User中提供更多功能:例如,将帐户标记为过期,并提供更可靠的密码检查,例如确认在最近五次密码更改中没有重复使用相同的密码。您可以通过使用继承来实现这一点。继承允许您采用现有类型,例如User,并通过使用extends
关键字子类化为其添加更多功能。继承类时,还可以通过重写现有方法和属性来为其指定新行为。
类继承:
子类与层次结构上的所有父类共享一个“is-a”关系。
您可以使用is
关键字在Dart中测试“is-a”关系。is关键字允许您检查对象的类型,并返回一个布尔值。
以下代码段显示,您可以使用与User对象相同的方式使用EnterpriseUser:
var user = new User("Alice");
print(user.username);
var enterpriseUser = new EnterpriseUser("Alice");
print(enterpriseUser.username);
EnterpriseUser的隐式接口也继承自超类。任何对象的公共接口都是继承类的所有公共接口的集合。
您现在可以添加特定于EnterpriseUser的功能,如下一个清单所示。这允许EnterpriseUser使用User提供的所有现有功能,但也提供了新功能。
class EnterpriseUser extends User {
EnterpriseUser() {}// Default, empty constructor
void markExpired() {// 新功能
// some new implementation
}
}
继承构造函数:
虽然继承允许子类从超类的方法和属性获得现有功能,但子类不会从超类继承任何构造函数。下图展示了EnterpriseUser的一些逻辑成员,它从用户类继承了方法和属性。
这意味着如果要在超类中使用构造函数,需要在EnterpriseUser中编写自己的构造函数,并使用super
关键字引用超类的构造函数。您可以在构造函数的一个称为构造函数初始值设定项块的特殊部分中执行此操作,该部分显示在构造函数参数列表和构造函数主体之间:
class EnterpriseUser extends User {
EnterpriseUser(String uname) : super(uname) {
// empty constructor body
}
}
构造函数初始值设定项是Dart在创建类实例之前执行的代码块。这是唯一可以调用超类构造函数的地方;不允许在构造函数主体中调用它们。还可以调用任何超类构造函数,包括命名构造函数。类中的参数列表不需要与超类的参数列表匹配。如果基类提供命名的超类构造函数,则可以使用超类作为构造函数名称前缀来调用其中任何一个。类似地,子类中的命名构造函数可以随意调用超类的构造函数:
class EnterpriseUser extends User {
EnterpriseUser(String uname, String email) : super.byEmail(email) {}// 继承父类的命名构造函数
EnterpriseUser.byUsername(String uname) : super(uname) {}// 继承父类的构造函数
}
构造函数初始化块是一个逗号分隔的命令列表,也可用于初始化final属性(如第6章所述),但对超类构造函数的任何调用都必须首先进行。
重写方法和属性:
继承父类时,将获得父类成员的所有功能。但有时您需要提供该功能的您自己的版本,例如当您希望能够在EnterpriseUser类中提供密码验证时。超类用户已经有一个isPasswordValid()方法,当您继承该类时,您将获得该方法的功能。但是,当Alice想要在您的企业系统中更改她的密码时,更改密码的要求比只比较新密码和旧密码更严格。Alice不能在五次密码更改中重复使用同一密码,这意味着在EnterpriseUser类中,您需要覆盖父用户类提供的功能:例如,记住最后五次密码哈希。幸运的是,通过提供具有相同名称和参数的新方法实现,这很容易做到,如下清单所示。通过再次使用super关键字引用基类中的同一函数(也在本清单中),仍然可以重用父用户类中提供的基础isPasswordValid()方法的继承功能。
class EnterpriseUser extends User {
bool isPasswordValid(String newPassword) {// 重写父类的方法(函数名称、返回值、参数列表,要完全相同)
// snip... compare against last 5 passwords
return super.isPasswordValid(newPassword);// 仍可调用父类老的功能函数
}
}
可以以类似的方式重写属性。属性只是getter或setter方法的简写,同样的原则也适用。User类提供了username属性,但是当Alice登录到您的企业系统时,您希望验证用户名以确保其长度超过四个字符。您可以通过提供getter和setter的新实现来覆盖原setter和getter,以执行此验证,如下清单所示。
class EnterpriseUser extends User {
String get username => super.username;
void set username(String value) {
if (value.length < 4) {
throw new ArgumentError("Error: username is < 4 chars");
}
super.username = value;
}
}
在类层次中包含抽象类:
您可以使用抽象类强制类的用户提供自己的实现方法,同时仍然提供自己的实现方法。
在示例系统中,当前有两个类构成层次结构。当Alice登录到开发人员系统上的系统时,她由User类表示。当她使用企业服务器登录到生产系统时,她由EnterpriseUser类表示,该类与其父类共享“is-a”关系。该层次结构如下图所示。
系统可以创建User类和EnterpriseUser类的实例,因为它们都是完整的类,完全定义了它们实现的接口所需的功能。开发人员可以独立于其任何子类使用User类。但通常在设计库时,您会希望强制使用某个类的开发人员提供一些自己的功能,因为在设计时您不知道需要哪些功能。
在示例User接口中,可以添加checkPasswordHistory()函数。此函数允许实现类在isPasswordValid()函数中检查密码历史记录。不幸的是,在设计用户类时,您不知道如何检查密码历史记录。您可以通过确保用户类的用户继承用户类并重写checkPasswordHistory()函数来提供自己的功能,从而强制用户类的用户提供该功能。要实现这一点,请在定义用户类时使用abstract关键字,如下清单所示。abstract关键字表示不能创建该类的新实例;相反,它允许继承它的子类完全满足这些要求。
abstract class User {// 显示接口定义
User(String this._username) {}
String _username;
String _existingPasswordHash;
String get username => _username;
String emailAddress;
void checkPasswordHistory(String newPassword);// 未实现的方法
bool isPasswordValid(String newPassword) {
// validate that the newPassword isn't the same
// as the existing password by comparing hashes
// 尽管类中不存在checkPasswordHistory()的实现,但您可以调用它,因为它将由子类实现
checkPasswordHistory(newPassword);
}
}
EnterpriseUser与User类具有“is-a”关系,它还需要将自己声明为抽象的或实现未实现的接口功能。下图显示了在EnterpriseUser类的实例上调用isPasswordValid()方法时如何使用实现。
Dart SDK的内置库充满了接口和继承;它们是开源的,在Dart编辑器中随时可用。
记住
■ extends关键字表示一个类正在继承(子类化)另一个类。
■ abstract关键字表示类没有提供自己的方法实现。继承抽象类的类应该提供该功能。
■ 子类不继承超类的构造函数。您可以使用super前缀在构造函数初始值设定项块中引用父类中的构造函数来调用它们。
■ 您还可以通过在普通代码中的任意位置使用super前缀来调用父类的特定方法和属性。
2.一切皆对象
Dart中的所有内容都是一个对象,它不同于Java和C#,在Java和C#中,有int和boolean等基本类型(以及Java.lang.Integer等对象等价物)。Object
类内置于Dart语言中,是除自身之外的所有类的最终父类。
在应用程序中创建变量实例时,无论是String、int还是自己的类(如EnterpriseAuthService),都是在创建Object
的实例。
你可以从两个方面来看待这个问题。首先,在面向对象编程中,创建对象的实例,从计算机科学的角度来看,对象是计算机内存中分配用于在运行的应用程序中存储实际数据的区域。这与类不同,类是Dart用于构造对象的源代码文件中的表示形式。
其次,Dart有一个Object
类,其他所有类都自动从该对象类继承。无论是否在类定义中使用extends关键字,都会发生这种情况。所有内置类和类型(如String、int、null和函数)都与Object
共享“is-an”关系。
print(Object is Object);
print(1 is Object);
print(false is Object);
print(null is Object);
print("Hello" is Object);
var someFunction = () => "";
print(someFunction is Object);
以上全部打印true,验证了“is-an”关系。
此继承在继承层次结构中一直起作用。当EnterpriseUser从User继承时,它也从基本Object类继承,但是是通过User类继承的。发生这种继承是因为继承提供了类层次结构上的“is-an”关系,这意味着每个子级与层次结构上的每个父级都有一个“is-an”关系,如下图所示。
当使用单个基类时,可以观察两点:
■ 每个类与对象都有一个“is-a”关系,这意味着您可以在变量和参数声明中将所有内容作为Object引用。
■ 每个类都继承基本对象类提供的一些基本功能。
使用 “is-an” Object关系:
doLogon(Object authService, String username, String password) {
if (authService is AuthService) {// 显式地检查输入参数的类型
return authService.auth(username,password);// 如果“is-an” AuthService,使用它
} else {
// throw exception
}
}
这样编写代码会使类型检查器无法验证传递给函数的变量是否正确。as
关键字允许您告诉类型检查器您希望将变量视为特定类型。例如,您可以从前面的代码段重写doLogon()函数中的返回行,如下所示:
if (authService is AuthService) {
return (authService as AuthService).auth(username, password);
}
但是,当您有一个函数想要采用不同的类型,而这些类型不共享另一个公共接口或其他公共父类时,它会很有用。在本例中,将参数声明为对象并检查函数中的类型是有效的:
processValue(Object someObject) {// 用Object这个最基础类型,很好地完成此功能
if (someObject is String) {
// snip
} else if (someObject is int) {
// snip
}
// etc...
}
⚠️您应该尽量避免在面向外部的代码中使用此模式,因为它没有为用户或工具提供有用的文档。如果您发现自己正在使用这种模式,请确保在代码中提供适当的注释来解释原因。
使用Object的toString():
Object类型提供了所有类都可以使用的少量方法和属性。最常用的方法是toString()方法,它提供实例的字符串表示形式。
例如,当您尝试将任何类的实例用作字符串时,通过将其传递到Dart的顶级print()函数,将调用Object的toString()方法。下面清单中的示例显式调用toString()函数,但如果不显式调用toString(),Dart将隐式调用它。
print(1.toString());// 1
print(1);// 1(toString()自动隐式被调用 like Java)
print("dart".toString());// dart
print(true.toString());// true
print(null.toString());// null
print(new Object().toString());// Instance of 'Object'
print(new User().toString());// Instance of 'User'
对Object调用toString()很有趣。它输出文本"Instance of ‘Object’",在该点上尽可能具有描述性。对User的实例调用toString(),则会得到"Instance of ‘User’"。因为您的类正在使用Object类中内置的功能。如果在不同的类(如List)上调用toString(),则会打印数字列表而不是"Instance of ‘List’",因为List类提供了自己的toString()方法,该方法会覆盖Object提供的功能。
通过向类定义中添加toString()函数,也可以在自己的类中重写此功能。例如,您可以让User类输出用户名,而不是使用Object.toString()提供的默认功能。如果添加日志记录功能,这样做会带来好处,因为您可以将用户实例传递到日志记录函数或顶级print()函数中,并返回描述性消息。下面清单显示了一个示例实现,该实现还使用了通过显式调用super.toString()在Object中找到的原始功能。
class User {
String username;
String toString() {
var myType = super.toString();
return "$myType: $username";
}
}
拦截noSuchMethod()调用:
Object类还公开了另一个有用的方法:noSuchMethod()
。与Java和C#不同(但与Ruby和Python等许多其他动态语言一样),您不局限于使用在所使用的类上显式声明的属性和方法。
当调用方法或访问类定义中不存在的属性时,Dart首先检查该方法或属性是否存在于层次结构上的任何父类中。如果没有,Dart将尝试查找在层次结构中声明的名为noSuchMethod()的方法。如果尚未显式定义该方法,Dart将调用Object.noSuchMethod()函数,该函数将抛出NoSuchMethodError。
您可以将此功能用于用户类层次结构。当Alice登录时,由于源系统中的数据不一致,您不能总是信任EnterpriseService返回的数据。与EnterpriseService团队达成的协议是,他们将返回尽可能多的数据,您将验证数据是否满足您的要求。因此,在任何User或EnterpriseUser实现类上都没有公开validate()方法,但有时您可能希望调用它。下图显示了Dart如何处理此调用。
当您调用validate()时,Dart会保持乐观,并期望您作为开发人员知道自己在做什么,然后尝试运行它。最后,Dart将在基本Object类中找到默认的noSuchMethod()实现,并抛出一个错误,该错误可以通过try/catch处理程序捕获,如以下代码段所示:
try {
user.validate("Alice");
} on NoSuchMethodError catch(err) {
// handle or ignore the error
}
通过实现自己版本的noSuchMethod(),可以防止抛出NoSuchMethodError。这种方法允许您截获丢失的方法调用并执行一些任意代码,例如验证数据。
noSuchMethod()接受两个参数:一个String表示您试图访问的方法的名称,另一个List包含您传递给该方法的参数列表。在下一章中,我们将更深入地讨论列表,但现在您需要知道列表是基于零的,并且您可以使用许多其他语言熟悉的方括号语法访问列表的元素。下面的清单显示了noSuchMethod(String name, List args)的示例实现。此示例代码打印出方法名和传递给该方法的参数数目。
class User {
noSuchMethod(String name, List args) {
print("$name, ${args.length}");
}
}
调用user.validate(“Alice”);,它导致字符串"validate, 1"被输出到控制台。
通过检查name参数的值并调用super.noSuchMethod(name, args)将调用传递给类层次结构,可以显式地检查方法名称并继续从基类抛出noSuchMethodError(如果需要)。这种方法允许您在忽略其他方法的同时捕获缺少的特定方法。
noSuchMethod()还可以拦截属性访问。假设您试图访问User上不存在的password字段。您可能希望忽略该set并为get返回一个星号字符串。当noSuchMethod()接收到对属性访问的调用时,它会在名称字段的前面加上get:
或set:
,这可以让您确定是访问getter还是setter。从而调用print(user.password);可以被以下的noSuchMethod()实现去处理:
noSuchMethod(name, args) {
if (name == "get:password") {
return "********";
} else if (name != "set:password") {// 既不是getter也不是setter,调用父类的noSuchMethod()
super.onSuchMethod(name, args);
}
}
Object类的其他默认功能:
在Object类中定义的连等号运算符在将对象实例与另一个对象实例进行比较时返回true/false值。也就是说,当比较两个变量是否相等时,如果它们是相同的实例,则返回true,如下图所示。
记住
■ 一切 “is-an” Object。
■ Object定义toString()和noSuchMethod(name, args)方法,您可以在自己的类中重写这些方法。
■ noSuchMethod()可以捕获未知的方法调用和属性访问。
3.动态类型(dynamic type)
我们将在本节中讨论的最后一种类型是dynamic
。在构建库(例如提供身份验证服务的logon_lib库)时,最好向其他开发人员和工具提供类型信息。这样做允许开发人员和工具从您选择的类型信息中推断出含义。在您的库中,或者在进行原型设计时,不指定任何类型信息是完全有效的。当您没有显式地给出任何类型信息时,例如当您使用var
关键字声明变量时,Dart使用一个称为dynamic
的特殊类型注释。
dynamic
类型在概念上与Object
类型相似,因为每个类型都 “is-a” dynamic
类型。因此,您可以在变量和参数声明中使用dynamic
类型注释,并将其用作函数返回类型。在语言方面,指定dynamic
类型等同于不提供类型信息;但是当您指定dynamic
类型时,您会让其他开发人员知道您决定使用dynamic
类型,而不仅仅是不指定它。我们将在本章后面更详细地了解这一点。
下图显示了在不提供其他类型信息的情况下如何自动使用dynamic
:在左侧使用dynamic
类型,在右侧使用显式类型:
“is-an”与dynamic工作的关系如何?
Dart中的每个类型都是一个对象,但每个类也都与dynamic(包括Object)有“is-an”关系。这是因为dynamic是每个其他类(包括Object类)实现的基本接口。dynamic接口不提供方法和属性,硬编码到虚拟机中,并且与Object不同,不能从中扩展或继承。
使用dynamic类型注释:
您可以显式地使用标识dynamic类型的dynamic关键字来代替其他类型信息;它通常用于表示您已明确决定不提供任何类型信息的情况。这种用法与Object的用法略有不同,Object是在您明确决定允许任何对象时使用的。当阅读其他人的代码时,您可以根据他们对Object或dynamic的选择做出下表中所示的解释。
但在实践中,除非您提供足够的代码注释来解释您使用dynamic的决定,否则应避免显式使用dynamic关键字。与所有规则一样,还有一个例外,当我们在第8章开始使用泛型时,我们将更深入地研究它。
总结
在本章中,我们介绍了Dart的继承,它允许您构建类的继承层次结构。Object是所有其他类和类型的父类,包括内置类,如String、int和null。另一方面,dynamic类型是Dart在运行时使用的类型,它允许代码在不影响执行代码的类型注释的情况下运行。
记住
■ 使用extends关键字声明类继承父类。
■ super关键字允许您调用父类上的成员(方法和属性)。
■ 您可以通过提供自己的实现来覆盖特定成员。
■ Object提供toString()方法,您可以使用该方法在输出日志消息时提供额外信息。
■ Object类中的noSuchMethod()可用于截获缺少的方法和属性。
■ dynamic类型注释表示Dart中变量和参数的非类型化版本。
下一章将介绍泛型,并讨论Dart的集合类,如Collection、List和Map。您还将学习如何通过自己实现泛型来创建灵活的通用类。您还将发现运算符重载,它通过自定义标准运算符中内置的含义,帮助您创建真正的自文档化代码。
以上是关于★Dart-7-扩展类和接口的主要内容,如果未能解决你的问题,请参考以下文章