Dart、FlutterDart语言中的注解
1. 注解的作用
注解也称元数据(metadata),用于在代码中添加的用于描述代码的额外的信息。注解可以应用于类、方法、属性、构造函数、函数参数等。
注解的作用是提供一种用于在 编译期 或 运行时 获取有关程序结构的附加信息的方法,从而可以实现更高级的编程技巧,例如自动序列化和反序列化、依赖注入等。
注解可以用于为代码添加额外的信息,它本身并不会影响程序的执行,但它们可以被编译器、静态分析工具或者运行时反射API所读取。这些工具可以根据注解提供的信息来执行特定的操作,比如:
- 生成额外的代码
- 检查代码的正确性
- 修改程序的行为
2. 注解的用法
注解的语法形式为:@Annotation
,可以直接放在需要添加注解的代码前面。一个代码元素可以有多个注解,每个注解之间用逗号隔开。例如:
class Person { void speak() { print('你好!'); } } class Man extends Person { @override void speak() { print('你好,世界!'); } }
这个例子中,@override
注解是一个内置注解 表示 Man 类的 speak
方法覆盖了 Person 类的 speak
方法。可以帮助编译器检查是否正确地覆盖了父类的方法。关于该内置注解将在下一节中进一步详细介绍。
2. 常见内置注解
如上一节中使用的 @override
注解,Dart 提供了一些内置的注解,可以直接在代码中使用。目前Dart语言中内置注解一共三个—— @Deprecated, @deprecated, 以及 @override
内置注解 | 描述 |
@Deprecated |
用于创建一个具有自定义消息和过时时间的过时注解。 |
@deprecated |
表示某个API已经过时,不建议使用。在使用过时的API时,静态分析工具会给出警告。 |
@override |
表示子类的方法覆盖了父类的方法。如果没有正确覆盖,静态分析工具会给出警告。 |
接下来将逐个解析更多的常见的内置注解。
2.1 @override
@override
注解用于表示子类的方法覆盖了父类的方法。这有助于在重构代码时确保正确地覆盖了父类方法。如果没有正确覆盖,Dart分析器会发出警告。
这个注解已经给出过例子,这里不重复给出。
2.2 @Deprecated
@Deprecated
注解用于表示一个类、方法或属性已被弃用,不建议使用。这有助于在升级库或框架时向开发者传达哪些功能已被弃用。使用已弃用的功能时,Dart分析器会发出警告。
例如:
class MyClass { @deprecated void oldMethod() { print('This method is deprecated'); } void newMethod() { print('Use this method instead'); } }
在这个示例中,我们将oldMethod
方法标记为已弃用,并推荐使用newMethod
方法。当开发者尝试使用oldMethod
时,Dart分析器会发出警告。
【注】:在旧的Dart版本中,使用的是 @deprecated,现在已经发被 @Deprecated 替代
3. meta库中提供的的常用注解
3.1 @required
@required
注解用于表示构造函数或方法的命名参数是必需的。这有助于确保在调用构造函数或方法时提供必需的参数。如果缺少必需的参数,Dart分析器会发出警告。
例如:
import 'package:meta/meta.dart'; class Person { final String name; MyClass({@required this.name}) { if (name == null) { throw ArgumentError('name is required'); } } } void main() { MyClass obj = Person(name: 'Jack'); // 正确使用 MyClass obj2 = Person(); // 分析器会发出警告,缺少必需的参数 }
在这个示例中,我们创建了一个MyClass
类,它有一个必需的命名参数name
。我们在构造函数参数上使用@required
注解来表示这一点。请注意,我们需要从package:meta/meta.dart
库中导入@required
注解。
3.2 @sealed
@sealed
注解用于表示一个类是密封的,即它只能在定义它的库中扩展。这有助于限制类的继承,确保库的内部实现不会被外部代码破坏。
例如:
// 在my_library.dart文件中 import 'package:meta/meta.dart'; @sealed class MyBaseClass { void myMethod() { print('This is a sealed class'); } } class MyDerivedClass extends MyBaseClass { // 这是允许的,因为MyDerivedClass和MyBaseClass在同一个库中 } // 在main.dart文件中 import 'my_library.dart'; class AnotherDerivedClass extends MyBaseClass { // 这是不允许的,因为AnotherDerivedClass和MyBaseClass在不同的库中 }
在这个示例中,我们创建了一个名为MyBaseClass
的密封类,并在同一个库中创建了一个名为MyDerivedClass
的子类。在另一个库中,我们尝试创建一个名为AnotherDerivedClass
的子类,但这是不允许的,因为MyBaseClass
是密封的。请注意,我们需要从package:meta/meta.dart
库中导入@sealed
注解。
3.3 @protected
@protected
注解用于表示一个类成员(方法、属性等)只能被其子类访问。这有助于实现封装,确保类的内部实现不会被其他类误用。
例如:
import 'package:flutter/foundation.dart'; class MyBaseClass { @protected void protectedMethod() { print('This is a protected method.'); } } class MyDerivedClass extends MyBaseClass { void callProtectedMethod() { // 可以在子类中访问受保护的方法 protectedMethod(); } } void main() { MyDerivedClass derivedClass = MyDerivedClass(); derivedClass.callProtectedMethod(); // 输出 "This is a protected method." }
在这个例子中,我们定义了一个基类 MyBaseClass
,它有一个受保护的方法 protectedMethod()
。我们使用 @protected
注解来表示这个方法只能在 MyBaseClass
及其子类中使用。然后,我们创建了一个继承自 MyBaseClass
的子类 MyDerivedClass
,并在其中调用了受保护的方法。
请注意,如果我们尝试在 MyDerivedClass
之外的类中调用 protectedMethod()
,Dart 分析器会给出警告,提示我们不应在子类之外使用受保护的成员。
3.2 @immutable
@immutable
用于标记一个 类 为不可变(Immutable)。不可变类是指 其实例在创建后不能被修改的类,所有字段都是final
的,且 没有公共的可变方法。这样的类可以提供更强的安全性和线程安全性。
使用 @immutable
注解可以让编译器检查类的不可变性,并防止对其进行修改。如果在不可变类中尝试修改字段的值或添加可变方法,编译器将会产生警告或错误。
@immutable class Person { final String name; final int age; Person(this.name, this.age); }
在上面的示例中,Person 类被标记为不可变,name
和 age
字段都是 final
的,且没有公共的可变方法。一旦创建了 Person 类的实例,其字段的值将不可更改。使用不可变类的好处如:
- 线程安全:不可变对象不需要担心并发修改的问题,可以在多线程环境中安全地共享。
- 可预测:由于不可变对象的值不会改变,可以在任何时候保证对象的状态一致性。
- 代码简化:不可变对象可以减少出错的机会,因为没有修改状态的可能性。
需要注意的是,@immutable注解只是一种元数据,它本身并不会强制类的不可变性。它主要用于提供给静态分析工具和代码生成工具使用,以检查和生成相关的代码。
3.5 @alwaysThrows
@alwaysThrows
注解表示一个函数总是抛出异常。这有助于静态分析工具了解代码的行为,以便更好地捕获潜在的错误。
例如:
import 'package:meta/meta.dart'; @alwaysThrows void throwError(String message) { throw Exception(message); } int foo(bool condition) { if (condition) { return 1; } else { throwError("Error occurred"); } } void main() { try { print(foo(false)); } catch (e) { print(e); // 输出 "Exception: Error occurred" } }
在这个例子中,我们定义了一个名为 throwError
的函数,它接受一个字符串参数并抛出一个异常。我们使用 @alwaysThrows
注解表示这个函数总是抛出异常。然后,我们在 foo
函数中使用 throwError
函数。由于我们使用了 @alwaysThrows
注解,Dart 静态分析器可以正确地识别在 foo
函数的 else
分支中,throwError
函数不会返回值。
使用 @alwaysThrows
注解可以帮助静态分析器更准确地分析代码,从而提高代码的可读性和可维护性。
4. 自定义注解
4.1 定义自定义注解语法(如何定义)
我们除了可以使用内置的注解外,其实我们还可以自定义注解。自定义注解需要创建一个类,并继承自Object
。类名通常以Annotation
为后缀。自定义注解类可以包含任意数量的参数,这些参数可以在使用注解时传入。要定义自定义注解,需要创建一个带有const
构造函数的类。这个类可以有任意数量的属性,但它们必须是编译时常量。
例如:
class MyAnnotation { final String description; const MyAnnotation(this.description); }
在这个示例中,我们定义了一个名为MyAnnotation
的自定义注解。这个注解有一个属性description
,用于存储与注解关联的文本描述。我们还为这个类提供了一个const
构造函数,以便在使用注解时可以创建编译时常量。
4.2 自定义注解用法(如何使用)
要使用自定义注解,只需在要注解的代码元素之前添加一个@
符号,后跟注解的名称和const
构造函数的调用。例如,可以将自定义注解应用于类、函数、变量等:
// 使用自定义注解注解类 @MyAnnotation('这是一个注解类的元数据') class MyClass { // ... } // 使用自定义注解注解函数 @MyAnnotation('这是一个注解函数的元数据') void myFunction() { // ... } // 使用自定义元素据注解变量 @MyAnnotation('这是一个注解变量的元数据') int myVariable;
在这个示例中,我们使用MyAnnotation
注解分别注解了一个类、一个函数和一个变量。注意,注解的值必须是编译时常量,因此我们使用const
构造函数来创建注解实例。
4.3 读取自定义注解
要读取自定义注解,可以使用反射(dart:mirrors
库)或代码生成工具(如source_gen
库)。以下是一个使用反射来读取自定义注解的示例:
import 'dart:mirrors'; void main() { // 获取MyClass的ClassMirror ClassMirror classMirror = reflectClass(MyClass); // 遍历MyClass的元数据 for (var metadata in classMirror.metadata) { // 检查元数据是否为MyAnnotation类型 if (metadata.reflectee is MyAnnotation) { // 获取MyAnnotation实例 MyAnnotation annotation = metadata.reflectee; // 输出注解的描述 print('MyClass annotation: ${annotation.description}'); } } }
在这个示例中,我们使用dart:mirrors
库中的reflectClass
函数来获取MyClass
的ClassMirror
。然后,我们遍历ClassMirror
的metadata
属性,检查每个元数据项是否为MyAnnotation
类型。如果找到一个匹配的注解,我们可以获取其description
属性并输出它。
请注意,由于dart:mirrors
库在Flutter中不受支持,因此在Flutter应用程序中需要使用代码生成工具(如source_gen
库)来读取和处理自定义注解。
5 使用注解的一些注意点
5.1 注解必须是常量表达式
注解的值必须是一个编译时常量表达式。这意味着注解不能包含运行时计算的值,例如变量、非常量函数调用等。常量表达式的要求确保了注解在编译时可以被解析,从而可以用于编译器优化、静态分析等。
// 示例:定义一个简单的自定义注解 class MyAnnotation { final String description; const MyAnnotation(this.description); } // 使用自定义注解(正确示例) @MyAnnotation('This is a constant description') class MyClass { // ... } // 使用自定义注解(错误示例) String runtimeDescription = 'This is a runtime description'; @MyAnnotation(runtimeDescription) // 错误:注解必须是常量表达式 class MyClass2 { // ... }
在上面的示例中,我们定义了一个简单的自定义注解MyAnnotation
,它接受一个字符串参数。在使用这个注解时,我们需要提供一个常量表达式作为参数。如果我们尝试使用一个运行时计算的值(例如变量runtimeDescription
),则会导致编译错误。
5.2 注解不能直接访问被注解对象的属性或方法
注解本身不能直接访问或修改被注解对象的属性或方法。要实现这一点,我们需要借助反射(dart:mirrors
库)或代码生成工具(如source_gen
库)。
class MyAnnotation { final String description; const MyAnnotation(this.description); // 错误示例:试图直接访问被注解对象的属性或方法 void printDescription(Object annotatedObject) { print(annotatedObject.description); // 错误:不能直接访问被注解对象的属性 } }
在上面的示例中,我们尝试在MyAnnotation
类中定义一个printDescription
方法,该方法试图直接访问被注解对象的description
属性。这是不允许的,因为注解不能直接访问被注解对象的属性或方法。
5.3 注解不会影响程序的执行
注解本身不会影响程序的执行。它们仅作为元数据提供给编译器、静态分析工具或运行时反射API。要利用注解实现某些功能,我们需要使用这些工具来读取和处理注解。
@MyAnnotation('This is a description') class MyClass { // ... } void main() { // 注解不会影响程序的执行 MyClass myObject = MyClass(); // ... }
在上面的示例中,我们给MyClass
类添加了一个MyAnnotation
注解。然而,这个注解不会影响程序的执行,也不会自动触发任何操作。要利用这个注解,我们需要使用相应的工具(例如反射或代码生成)来读取和处理它。