Dart学习笔记(一)语言基础
作为Flutter和下一代系统Fuchsia‘钦定’的语言,Dart还是有兴了解一下的。
安装方式,在已经有brew的macOS系统下很简单:
$ brew tap dart-lang/dart
$ brew install dart
如需升级更新Dart SDK版本的话,直接使用brew upgrade dart
即可。
查看已安装dart版本信息,使用brew info dart
一般来说学新语言都会有个hello world程序,主要目的在于观察这种语言与已掌握语言有哪些地方长得像,又有哪些地方长得不像。其实各种语言也许底层实现、编译翻译方式、SDK函数API及一些高级用法会有很大差别,但最基本的地方往往彼此彼此。下面就是官方给的一个小例子:
// 定义函数
printInteger(int aNumber) {
print('The number is $aNumber.'); // 打印到控制台
}
// 应用开始执行的地方
main() {
var number = 42; // 声明及初始化变量
printInteger(number); // 调用函数
}
由上面例子对于Dart程序基本风格也就可见一斑了。
重要概念
- 能放进一个变量的任何东西都是一个对象,而每个对象都是一个类的实例。即使是数、函数和
null
这样的也是对象。所有的对象继承自Object
类 - 尽管Dart是强类型的,类型注解却因为Dart可自推断类型而是可选的(如上面小例子中
number
变量的类型会被推断为int
)。当需要明确指定无类型时,使用特殊类型dynamic
。 - Dart支持泛型,如
List<int>
或List<dynamic>
- Dart支持顶级函数(如
main()
),亦或函数绑定给一个类或对象(分别为静态static
和实例instance
方法)。你也可以在函数内创建函数(内嵌nested
或本地local
函数) - 类似的,Dart支持顶级变量、类或对象的变量。实例变量即域或者属性。【好吧,其实只是名字怎么叫的问题】
- 不同于Java的是,Dart没有诸如public、protected、private这样的关键字。当标识符以下划线(_)开始,即表示在它的库中为私有。详见
Libraries and visibility
- 标识符可以字母或下划线开头,后跟字母数字下划线的组合。(和其他大部分语言一样)
- Dart有表达式和语句。(学过其他语言的这个没什么好解释的,辛苦官网还特意举例解释了。。。)
- Dart工具可以报告warning和error。(好吧,也是废话。。。)
关键字
官网列举了Dart语言的关键字,告知应当避免以它们为标识符。还说除了保留字,另外一些关键字可以用作标识符。然而这一般情况下没什么意义,尤其是对于初学者而言,在玩出这些花之前,标识符(变量、函数、类、对象等的名称)的选取还是应当以准确表意为妥,也就会自然避开不可用关键字。
变量
变量存储引用,如var name = 'Bob';
中name
含有到值为“Bob”的字符串对象的引用。name
变量的类型被推断为String
,但你可以通过指定来改变它的类型。
默认值
未初始化的变量有初始值null,包括数值类型的。(因为数值像其他Dart中所有事物一样是对象)
int lineCount;
assert(lineCount == null);
生产代码忽略assert()
的调用。在开发中,assert(condition)
在condition为false时抛出异常。
final和const
如果永远不改变一个变量,使用final
或const
来替代var
或添加在类型前。一个final变量只能被赋值一次,一个const变量是编译时常量(自然也暗含final之意)。final的顶级或类变量在首次使用时初始化。
实例变量可以是final
但不能是const
如果const变量是类级的,标记为static const
。const变量需要在声明时设置它的值。
内建类型
数值类型
Dart内数值类型有两种,int
和double
。int
类型不大于64位(8字节),这取决于平台。Dart VM中值可以从-263到263-1,若编译到JavaScript则使用JS的数值范围为-253到253-1double
为IEEE 754标准规定的64位双精度浮点数值。
这两种都是num
类型的子类型。包含基本运算符+-*/,也可使用abs()
、ceil()
、floor()
等方法。(位移运算符如>>定义在int
类中)
自Dart2.1开始,int型在必要时会自动转换为double型。
下面是常见的数值和字符串的转换:
// String -> int
var one = int.parse('1');
assert(one == 1);
// String -> double
var onePointOne = double.parse('1.1');
assert(onePointOne == 1.1);
// int -> String
String oneAsString = 1.toString();
assert(oneAsString == '1');
// double -> String
String piAsString = 3.14159.toStringAsFixed(2);
assert(piAsString == '3.14');
字面数值是编译时常量,许多算术表达式在其操作数为编译时常量时也是编译时常量,会被计算为数值。
字符串
Dart中字符串是UTF16编码单元的串。可以使用单引号或者双引号创建。
可以使用${expression}
讲表达式值放进字符串中,对于对象Dart会调用对象的toString()
方法。
==
运算符测试两个对象是否相等,对于两个字符串在它们包含相同编码单元序列时相等。
对于字符串的连接,可以使用+
运算符或是简单的让字符串相邻。
创建多行字符串使用三引号如'''
或"""
可以在字符串引号前加r
来创建raw字符串,raw字符串不会对内容进行转义。
布尔型
Dart有bool
类型用以表达布尔值,true和false。
Dart的类型安全不允许使用诸如if (非布尔类型值)
或者assert (非布尔类型值)
,对一些特殊值的检查必须明确:
// 检查空字符串
var fullName = '';
assert(fullName.isEmpty);
// 检查0值
var hitPoints = 0;
assert(hitPoints <= 0);
// 检查null.
var unicorn;
assert(unicorn == null);
// 检查NaN.
var iMeantToDoThis = 0 / 0;
assert(iMeantToDoThis.isNaN);
列表
在Dart中,数组为List
对象,所以被直接称为列表。Dart的列表形式类似JSvar list = [1, 2, 3];
,在Dart中这个列表被推断为List<int>
类型,所以如果尝试为它添加非整型对象,分析器或运行时会报错。
列表使用0基索引(类C),可以像JS中一样去获取列表长度或指定元素(如list.length
和list[1]
)。
Dart2.3起引入了展开操作符(...
)和空感知展开操作符(...?
)以提供简明方式插入多元素。
var list = [1, 2, 3];
var list2 = [0, ...list];
assert(list2.length == 4);
var list;
var list2 = [0, ...?list];
assert(list2.length == 1);
Dart2.3同时还引入了收集if(collection if)和收集for,以根据条件或重复构建集合。
//收集if
var nav = [
'Home',
'Furniture',
'Plants',
if (promoActive) 'Outlet'
];
//收集for
var listOfInts = [1, 2, 3];
var listOfStrings = [
'#0',
for (var i in listOfInts) '#$i'
];
assert(listOfStrings[1] == '#1');
集合
Dart中集合是不重复项目的无序收集。(集合的字面表达在Dart2.2才引入)
var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine'};
上面这个集合会被自动推断为Set<String>
,便不能添加其他错误类型值。
关于创建空集合,参见下面:
var names = <String>{};
// Set<String> names = {}; //这样亦可
// var names = {}; //这样创建的是一个map而非集合,推断为 Map<dynamic,dynamic>
使用.length
获取集合元素数量。使用.add(元素)
或.addAll(集合)
来添加项目。
与前面的列表类似,集合也支持展开操作符和收集if、收集for等。
Maps(映射?)
一个map是关于键与值之间的关联,键和值都可以是任意类型对象。每个键只能出现一次,但值可相同。
map的创建可以直接赋值创建,也可以通过Map构造器创建:
var nobleGases = {
2: 'helium',
10: 'neon',
18: 'argon',
};
var gifts = Map();
gifts['first'] = 'partridge';
gifts['second'] = 'turtledoves';
gifts['fifth'] = 'golden rings';
上面你可能本来期待看到的是new Map()
,但Dart2中new
关键字是可选的。
上面的创建中其实也体现了该如何往map中添加新键值对,取值方法类似,都和JS中一样。如果所取的键在map中不存在,则返回null
。同样支持.length
得到map中键值对的数量。
和列表集合类似,也支持扩展操作符、收集if和收集for等。
Runes和grapheme clusters
Unicode编码通过对于世界上所有的书写系统的每个字符进行独特数字编码。因为Dart字符串是UTF16编码单元序列。通常表达Unicode编码点的方式为\uXXXX,当不足或多于4位16进制时采用\u{XXXXX}类似的方式。
如果需要读写单独的Unicode字符,使用字符包中定义在String上的characters
获取器,返回的为Characters
对象,例子如下:
import 'package:characters/characters.dart';
...
var hi = 'Hi 🇩🇰';
print(hi);
print('The end of the string: ${hi.substring(hi.length - 1)}');
print('The last character: ${hi.characters.last}\n');
输出结果取决于运行环境,可能是类似下面这样:
$ dart bin/main.dart
Hi 🇩🇰
The end of the string: ???
The last character: 🇩🇰
符号
Symbol
对象代表Dart中声明的一个操作符或标识符。这个可能永远也不会用到。😅
函数
Dart是真面向对象语言,所以甚至函数都是对象并有类型Function
。这意味着函数可以被赋值给变量或作为参数传给其他函数。你也可以当做一个函数来调用Dart类的实例。下面是一个函数的实现:
bool isNoble(int atomicNumber) {
return _nobleGases[atomicNumber] != null;
}
尽管高效Dart推荐对公共API注解类型,但也可以省略类型:
isNoble(atomicNumber) {
return _nobleGases[atomicNumber] != null;
}
只有一个表达式的函数,可以用简短符号:
bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;
符号=> expr
是{ return expr; }
的简略表达。
只有表达式(而非语句)可以出现在=>和;之间
函数可以有两种参数:必须的和可选的。必须参数在前,可选参数在后。可选参数可以是命名参数
(named
)或位置参数
(positional
)。
可选参数
命名参数
命名参数的定义和使用时形式如下:
void enableFlags({bool bold, bool hidden}) {...}
enableFlags(bold: true, hidden: false);
尽管命名参数是可选参数,但可通过注解@required
来指示参数是必须的(依赖meta
包,需import package:meta/meta.dart
),如:
const Scrollbar({Key key, Widget child})
位置参数
用方括号标记参数为可选:
String say(String from, String msg, [String device]) {
var result = '$from says $msg';
if (device != null) {
result = '$result with a $device';
}
return result;
}
assert (say('Bob', 'Howdy') == 'Bob says Howdy');
assert (say('Bob', 'Howdy', 'smoke signal') == 'Bob says Howdy with a smoke signal');
参数默认值
可以在定义函数时,使用=
来定义参数默认值(无提供默认值时默认值为null
)。
main()函数
每个应用必须有一个顶层main()
函数作为应用的入口。其返回值为void
且有一个可选参数(para)List<String>
作为参数(args)。
函数作为头等对象
可以传递函数给另一个函数作为参数:
void printElement(int element) {
print(element);
}
var list = [1, 2, 3];
// 将printElement作为参数传递
list.forEach(printElement);
也可赋值函数给变量:
var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!';
assert(loudify('hello') == '!!! HELLO !!!');
匿名函数
大部分函数是命名的,也可以创建没有名字的匿名函数(anonymous
),或者有时叫做lambda
或closure
(闭包)
var list = ['apples', 'bananas', 'oranges'];
list.forEach((item) {
print('${list.indexOf(item)}: $item');
});
// list.forEach(
// (item) => print('${list.indexOf(item)}: $item'));
// 匿名函数这里=>后可以跟唯一的语句,注意和前面简略写法处区分
词法范围
Dart是词法范围语言,所以变量的作用域被代码静态的确定。
词法闭包
词法闭包(closure)是函数在词法范围可访问一个变量而在原范围之外被调用。
参照下面例子理解:
Function makeAdder(num addBy) {
return (num i) => addBy + i;
}
void main() {
var add2 = makeAdder(2);
var add4 = makeAdder(4);
assert(add2(3) == 5);
assert(add4(3) == 7);
}
函数相等的判断
void foo() {}
class A {
static void bar() {}
void baz() {}
}
void main() {
var x;
x = foo;
assert(foo == x);
x = A.bar;
assert(A.bar == x);
var v = A(); // A的实例1
var w = A(); // A的实例2,注意v与w的不同
var y = w;
x = w.baz;
assert(y.baz == x);
assert(v.baz != w.baz);
}
返回值
所有函数返回一个值,未指明的话return null;
隐含加在函数体后。
foo() {}
assert(foo() == null);
操作符
算术操作符
基本和C一致(-expr
取反,~/
整除)。
关系操作符
基本和C一致。
一般测试x、y代表的是否一样的事物使用==号(在少数情况下需要知道两个对象是否完全同一对象,使用identical()
函数)。==操作符运作方式:
- 如果x或y是null,当两个都为null时为true否则为false
- 返回
x.==(y)
的结果(即==等操作符是第一个操作数的方法,可以被重写)
类型测试操作符
as
、is
和is!
为在运行时测试类型的操作符。
操作符 | 含义 |
---|---|
as |
类型转换,如(emp as Person).firstName = 'Bob'; |
is |
判断对象是否为指定类型 |
is! |
判断对象是否非指定类型 |
赋值操作符
除了常规的赋值操作符,还有只在被赋值变量为null时才赋值的??=
操作符。
另外复合赋值操作符类似C,如+=
。
逻辑操作符(类C)
位操作符(类C)
条件表达式
有C中的三目算符condition ? expr1 : expr2
。同时还有个双目的expr1 ?? expr2
,表示expr1不为null则返回expr1,否则返回expr2(可以理解为是有默认值的取值)。
String playerName(String name) => name ?? 'Guest';
级联注解(级联操作符..)
级联操作符(..
)允许制造在同一个对象上的操作序列,通常可以节省创建临时变量的步骤。
querySelector('#confirm') // 取得对象
..text = 'Confirm' // 使用成员
..classes.add('important')
..onClick.listen((e) => window.alert('Confirmed!'));
上面这段等同于下面这段:
var button = querySelector('#confirm');
button.text = 'Confirm';
button.classes.add('important');
button.onClick.listen((e) => window.alert('Confirmed!'));
其他操作符
操作符 | 名称 | 含义 |
---|---|---|
() |
函数调用 | 代表函数调用 |
[] |
列表访问 | 取得列表中指定索引处的值 |
. |
成员访问 | 取得表达式的一个属性 |
?. |
条件成员访问 | 类似成员访问,但先判断表达式是否null,如null则返回null |
控制流语句
if 和 else
没什么特别的,需要注意条件必须是布尔型值即可。
for循环
下面是一个示例,for中的闭包会捕捉索引的值(而非JS一样只传递变量引用),输出值为0和1:
var callbacks = [];
for (var i = 0; i < 2; i++) {
callbacks.add(() => print(i));
}
callbacks.forEach((c) => c());
如果要迭代访问的对象是可迭代的,可以使用forEach()
方法(如果不关心当前迭代计数的话):
candidates.forEach((candidate) => candidate.interview());
可迭代类如List和Set也支持for-in
形式进行迭代:
var collection = [0, 1, 2];
for (var x in collection) {
print(x); // 0 1 2
}
while 和 do-while
没什么特别的。
break 和 continue
没什么特别的。
switch 和 case
没什么特别的。当一个case结束希望继续执行别的case,可在别的case前一行加label:
,然后使用continue label;
assert
在开发中,使用assert(condition, optionalMessage);
来在条件为false时中断执行。
assert是否生效,取决于工具和框架设置:
- Flutter在debug模式使能
- 仅开发工具如dartdevc一般默认使能
- 一些工具,如dart、dart2js,通过命令行标志
--enable-assert
使能
在生产代码中assert被忽略。
异常
Dart代码可以抛出捕获异常。若异常未被捕获,则产生异常的隔离(isolate)被挂起,一般隔离和它的程序被终止。
相比Java,Dart所有的异常都是不受控异常,方法不声明它们可能抛出的异常,你也不被要求捕获任何异常。
Dart提供Exception
和Error
类型及很多预定义子类型。你也可以定义自己的异常。Dart可以将任何非null对象(而非仅Exception和Error)作为异常抛出。
抛出(throw)
没什么特别的。
捕获(catch)
一般有try-on
、try-catch
两种方式,当需要指定特定类型异常时使用on
,在需要得到异常对象时使用catch
。catch
可以被指定一个或两个参数,第一个是被抛出的异常对象,第二个是栈轨迹(StackTrace
对象)。
try {
breedMoreLlamas();
} on OutOfLlamasException {
buyMoreLlamas();
}
try {
breedMoreLlamas();
} on OutOfLlamasException {
// 指定的异常
buyMoreLlamas();
} on Exception catch (e) {
// 任何其他的Exception类型异常
print('Unknown exception: $e');
} catch (e, s) {
// 不指定,处理所有异常
print('Something really unknown: $e');
print('Stack trace:\n $s');
}
当部分处理异常,令异常传送,可以使用rethrow
关键字,即让调用者看到这个异常:
void misbehave() {
try {
dynamic foo = true;
print(foo++); // 此处运行时错误
} catch (e) {
print('misbehave() partially handled ${e.runtimeType}.');
rethrow; // 允许调用者看到这个异常
}
}
finally
为确保一些代码在无论异常是否抛出时都被执行,使用finally
。如果没catch语句符合异常,异常在finally执行后传送。
类
使用类成员
前文操作符处介绍过的.
和?.
即可
使用构造器
可使用构造器创建对象,构造器名字可以是类名
或类名.标识符
如:
var p1 = new Point(2, 2);
var p2 = new Point.fromJson({'x': 1, 'y': 2});
其中new
是可选的(自Dart2始)。
一些类提供常量构造器,使用const
关键字来创建运行时常量:
var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);
assert(identical(a, b)); // 它们是相同的实例!
在常量上下文中,可以忽略构造器前的const
,例如上面var
替成const
则后面构造器前的const
可以省略。(自Dart2始)
取得对象类型
可使用对象的runtimeType
属性(返回Type
对象)来在运行时得到对象的类型。
实例变量
没有什么特别的。
构造器
通过创建与类同名函数声明构造器(也可选择加上附加的标识,类似前文使用构造器所述的类名.标识符
)。其中,同样可用this
关键字表示当前的实例(一般只在有命名冲突时使用,否则省略this)。
将构造器参数传给实例变量很普遍,Dart就有了如下语法糖:
class Point {
num x, y;
// 语法糖,在构造器体运行前设置x、y值
Point(this.x, this.y);
}
默认构造器
未声明构造器,则提供默认构造器,该构造器无参数并调用父类中无参数构造器。
构造器非继承
子类不从父类继承构造器。
命名构造器
如使用构造器中的添加标识符的构造器:
class Point {
num x, y;
Point(this.x, this.y);
Point.origin() {
x = 0;
y = 0;
}
}
调用非默认父类构造器
默认的,子类构造器在构造器体开始处调用父类无命名、无参数构造器。如果初始化列表被使用则在调用父类的前执行。总结如下:
- 初始化列表
- 父类无参构造器
- 主类无参构造器
若父类没有无命名、无参构造器,则你必须手动调用父类中的一个构造器,在构造器体(如果有的话)前用冒号(:)指定:
class Person {
String firstName;
Person.fromJson(Map data) {
print('in Person');
}
}
class Employee extends Person {
// Person 无默认构造器,你必须调用 super.fromJson(data).
Employee.fromJson(Map data) : super.fromJson(data) {
print('in Employee');
}
}
给到父类构造器的参数无法访问this
。例如,参数可以调用静态方法但不能调用实例方法。
初始化列表
除调用父类构造器之外,还可以在构造器体之前初始化实例变量,其间用逗号分隔:
Point.fromJson(Map<String, num> json)
: x = json['x'],
y = json['y'] {
print('In Point.fromJson(): ($x, $y)');
}
初始化左端不能访问到this
。
另外,在开发中也可以在初始化列表当中使用assert
验证输入值。
重定向构造器
有时一个构造器只要重定向到同类中另一个构造器,其无构造器体,在冒号后调用重定向到的构造器:
class Point {
num x, y;
Point(this.x, this.y);
Point.alongXAxis(num x) : this(x, 0);
}
常量构造器
若类产生对象从不改变,可以使这些对象为编译时常量。定义const
构造器并确保所有实例变量为final
:
class ImmutablePoint {
static final ImmutablePoint origin =
const ImmutablePoint(0, 0);
final num x, y;
const ImmutablePoint(this.x, this.y);
}
常量构造器并不总是创造常量。
工厂构造器
使用factory
关键字,以实现一个构造器不总是创建它的类的新实例,如一个工厂构造器可能返回缓存中的实例或者子类型的一个实例:
class Logger {
final String name;
bool mute = false;
// 注意 _cache 是私有库(因为_开头)
static final Map<String, Logger> _cache =
<String, Logger>{};
factory Logger(String name) {
return _cache.putIfAbsent(
name, () => Logger._internal(name));
}
Logger._internal(this.name);
void log(String msg) {
if (!mute) print(msg);
}
}
工厂构造器不能访问this
方法
实例方法
实例方法可以访问实例变量和this
。
getter 和 setter
getter和setter是读写对象属性的特殊方法,每个实例变量都有隐式的getter,以及合适的话还有个setter。你可以实现getter、setter来创建额外的属性,使用get
和set
关键字:
class Rectangle {
num left, top, width, height;
Rectangle(this.left, this.top, this.width, this.height);
// 定义两个计算属性: right and bottom.
num get right => left + width;
set right(num value) => left = value - width;
num get bottom => top + height;
set bottom(num value) => top = value - height;
}
void main() {
var rect = Rectangle(3, 4, 20, 15);
assert(rect.left == 3);
rect.right = 12;
assert(rect.left == -8);
}
如自增(++)这样的操作符无论是否getter被明确定义都按照期待方式运作。为避免任何未期待的影响,操作符调用getter后将值存在一个临时变量中。
抽象方法
抽象方法只能在抽象类中,用分号(;)代替方法体即创造了个抽象方法:
abstract class Doer {
void doSomething(); // 定义抽象方法
}
class EffectiveDoer extends Doer {
void doSomething() {
}
}
抽象类
参见抽象方法,抽象类不能实例化。若想抽象类可实例化,可定义工厂构造器。
隐式接口
每个类隐含定义一个接口包含类的所有实例成员及它实现的任何接口。如果你想创建类A支持B的API而不继承B的实现,类A应当实现B的接口。
class Person {
// 接口,但仅库中可见
final _name;
// 构造器,非接口
Person(this._name);
// 接口
String greet(String who) => 'Hello, $who. I am $_name.';
}
// Person接口的的一个实现
class Impostor implements Person {
get _name => '';
String greet(String who) => 'Hi $who. Do you know who I am?';
}
String greetBob(Person person) => person.greet('Bob');
void main() {
print(greetBob(Person('Kathy')));
print(greetBob(Impostor()));
}
下面是指定类实现多个接口的例子:
class Point implements Comparable, Location {...}
类继承
使用extends
创建子类,并用super
来引用父类。
重写(overriding)成员
子类可以重写实例方法、getter、setter。可用@override
注解指明要重写的成员:
class SmartTelevision extends Television {
void turnOn() {...}
// ···
}
代码中收窄方法参数或实例变量的类型是类型安全的,你可以使用convariant
关键词在子类型前来表明此处应用子类型的意图。
可重写操作符
下面是个重写操作符的例子:
class Vector {
final int x, y;
Vector(this.x, this.y);
Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
Vector operator -(Vector v) => Vector(x - v.x, y - v.y);
// 操作符==与hashCode代码未列出,见随后的说明
}
void main() {
final v = Vector(2, 3);
final w = Vector(2, 2);
assert(v + w == Vector(4, 5));
assert(v - w == Vector(0, 1));
}
如果要重写==,就也需要重写对象的hashCode
的getter。类似下面:
class Person {
final String firstName, lastName;
Person(this.firstName, this.lastName);
// 重写hashCode
int get hashCode {
int result = 17;
result = 37 * result + firstName.hashCode;
result = 37 * result + lastName.hashCode;
return result;
}
// 重写hashCode后通常应重写==
bool operator ==(dynamic other) {
if (other is! Person) return false;
Person person = other;
return (person.firstName == firstName &&
person.lastName == lastName);
}
}
void main() {
var p1 = Person('Bob', 'Smith');
var p2 = Person('Bob', 'Smith');
var p3 = 'not a person';
assert(p1.hashCode == p2.hashCode);
assert(p1 == p2);
assert(p1 != p3);
}
noSuchMethod()
可重写noSuchMethod()
方法来在使用不存在方法或实例变量时做出反应:
class A {
// 除非重写noSuchMethod,否则使用不存在成员产生个NoSuchMethodError
void noSuchMethod(Invocation invocation) {
print('You tried to use a non-existent member: ' +
'${invocation.memberName}');
}
}
除非下列情形,否则不可调用未实现的方法:
- 接收者有静态类型
dynamic
- 接收者有静态类型定义了这个未实现方法(可以是抽象的),且接收者的动态类型有实现
noSuchMethod()
不同于Object
类