0717PL/SQL第二章第四章学习内容

第二章 PL/SQL基础

一、字符集

在PL/SQL程序中,允许出现的字符集包括:

  1. 大小写字母(A-Z和a-z)
  2. 数字(0-9)
  3. 符号( ) + - * / < > = ! ~ ^ ; : . ’ @ % , " # $ & _ | { } ? [ ]
  4. 制表符、空格和回车符

PL/SQL对大小写不敏感,所以,除了在字符串和字符中,小写字母和它对应的大写字母是等价的。

二、词法单元

PL/SQL包含很多词法单元(lexical unit),大致可以分为以下几类:

  1. 分隔符(简单符号和复合符号)
  2. 标识符,其中包括关键字
  3. 文字
  4. 注释

为改善可读性,我们可以用空格将词法单元分隔开。实际上,我们必须将相邻的两个标识符用空格或标点符号隔开。下面这样的写法是不允许的,因为关键字END和IF连到一起了:

IF x > y tdEN high := x; ENDIF; -- not allowed

还有,除了字符串和注释以外,我们不可以在词法单元中嵌入空格。例如,像下面的赋值符号中间就不用被分开:

count : = count + 1; -- not allowed

为了让层次结构清楚,我们可以用回车符来换行,空格或制表符来进行缩进。比较一下下面两段IF语句的可读性:

IF x>y tdEN max:=x;ELSE max:=y;END IF; IF x > y tdEN
  MAX    := x;
ELSE
  MAX    := y;
END IF;

1、分隔符

分隔符是对PL/SQL有着特殊意义的简单或复合的符号。例如,我们使用加号和减号这样的分隔符来表现数学运算。简单分隔符只有一个字符。

符号含义
+ 加法操作符
% 属性指示符
字符串分隔符
. 组件选择器
/ 触法操作符
( 表达式或列表分隔符
) 表达式或列表分隔符
: 主变量指示符
, 分隔符
* 多应用程序操作符
" 引用标识符分隔符
= 关系操作符
< 关系操作符
> 关系操作符
@ 远程访问指示符
; 语句终结符
- 减号/负号操作符

复合分割符由两个字符组成。

符号含义
:= 赋值操作符
=> 管联操作符
|| 连接操作符
** 求幂操作符
<< 标签分隔符(开始)
>> 标签分隔符(结束)
/* 多行注视分隔符(开始)
*/ 多行注视分隔符(结束)
.. 范围操作符
<> 关系操作符
!= 关系操作符
~= 关系操作符
^= 关系操作符
<= 关系操作符
>= 关系操作符
-- 单行注释提示符

2、标识符

我们可以使用标识符来为PL/SQL程序中的常量、变量、异常、游标、游标变量、子程序和包命名。下面是一些标识符的例子:

  1. X
  2. t2
  3. phone#
  4. credit_limit
  5. LastName
  6. oracle$number

标识符可以由字母、数字、美元符号($)、下划线(_)和数字符号(#)组成。而像连字符(-)、斜线(/)等符号都是不允许使用的。如下例:

  1. mine&yours -- 不允许使用连字符(not allowed because of ampersand)
  2. debit-amount -- 不允许使用连字符(not allowed because of hyphen)
  3. on/off -- 不允许使用斜线(not allowed because of slash)
  4. user id -- 不允许使用空格(not allowed because of space)

而使用美元符号、下划线和数字符号都是允许的:

  1. money$$$tree
  2. SN##
  3. try_again_

我们也可以使用大小写混合的形式来编写标识符。但是要记住,除了字符串和字符以外,PL/SQL对大小写是不敏感的。所以,只在大小写上有区别的标识符,PL/SQL会把它们当做同一标识处理,如下例:

  1. lastname
  2. LastName -- 与lastname相同
  3. LASTNAME -- 与lastname和Lastname相同

标识符的长度不能超过30。对于标识符的命名尽可能代表某种含义,避免使用像cpm这样的命名,而是使用cost_per_tdousand这样意义明确的命名方式。

  • 保留关键字

对于某些标识符,我们称它们为保留关键字(reserved word),因为对于PL/SQL来说,它们有着特殊含义,不可以被重新定义。例如BEGIN和END,它们代表块或子程序的起始和结束而被PL/SQL保留下来。在下面的例子中,我们可以看到,如果重定义一个关键字的话,就会产生一个编译错误:

DECLARE
  end BOOLEAN-- not allowed; causes compilation error

但像下面这样把保留关键字嵌套在标识符中使用是允许的:

DECLARE
  end_of_game BOOLEAN-- allowed

通常,保留关键字都是以大写形式存在的,这样能够增强可读性。但是,跟其他PL/SQL标识符一样,保留关键字也可以使用小写或大小写混合的形式。

  • 预定义标识

在包STANDARD中声明的全局标识符(如INVALID_NUMBER)是可以被重新声明的。但是,不建议重新声明预定义标识符,因为这样做的结果会使本地声明覆盖全局声明。

  • 引用标识符

为了获取更多的灵活性,PL/SQL允许我们用双引号将标识符夹起来。这样的标识符很少使用,但有时它们非常有用。它们可以包含任何可打印字符,其中空格也包含在内,但是,不可以包含双引号。因此,下面这些引用标识符都是有效的:

  1. "X+Y"
  2. "last name"
  3. "on/off switch"
  4. "employee(s)"
  5. "*** header info ***"

除了双引号以外,引用标识符最多可以包含30个字符。虽然把PL/SQL保留关键字作为引用标识符是被允许的,但这并不是一个好的编程习惯。

有些PL/SQL保留关键字并不是SQL的保留关键字。例如,我们可以在CREATE TABLE语句中使用TYPE作为字段名。但是,如果程序中的SQL语句要引用到这个字段的话,就会发生编译错误:

SELECT acct, type, bal INTO ... -- causes compilation error

为了避免发生这样的错误,就需要把字段名用双引号夹起来:

SELECT acct, "TYPE", bal INTO ...

要注意的是,字段名不能采用小写或大小写混合的形式(CREATE TABLE语句中除外)。例如,下面的语句是无效的:

SELECT acct, "type", bal INTO ... -- causes compilation error

还有一种做法就是可以建立视图来为原来的字段名更换一个新名。

3、文字

文字就是一个数字、字符、字符串或布尔(Boolean)值。它本身是数据而不是对数据的引用,如数字147和布尔值FALSE都是文字。

  • 数字文字

在算术表达式中有两种数字文字可以使用:整数和实数。整数文字不带小数点,有一个可选的符号,例子如下:

030 6 -14 0 +32767

实数文字带有小数点,也有一个可选的符号,例子如下:

6.6667 0.0 -12.0 3.14159 +8300.00 .5 25.

PL/SQL把12.0和25.这样的数字都当作实数处理,虽然它们只有整数部分值。

数字文字不能包含美元符号或是逗号,但可以使用科学记数法。只要在数字后面添加一个E(或e),再跟上一个整数即可(符号可选)。比如下面几个例子:

2E5 1.0E-7 3.14159e0 -1E38 -9.5e-3

E代表了十的幂,即权(times ten to tde power of)。E后面的整数值代表指数。**是幂操作符。

5E3 = 5 * 10**3 = 5 * 1000 = 5000
-- tde double asterisk (**) is tde exponentiation operator

在上面的例子里,小数点向右移动三个位置,而在下面这个例子中,我们把E后面的数字改成-3,就能让小数点向左移动三个位置:

5E-3 = 5 * 10**-3 = 5 * 0.001 = 0.005

再举一个例子。如果字符文字的范围不在1E-130到10E125之间,就会产生编译错误:

DECLARE
  n NUMBER;
BEGIN
  n := 10E127;   -- causes a 'numeric overflow or underflow' error
  • 字符文字

字符文字就是由单引号夹起来的一个单独的字符。字符文字包括PL/SQL字符集中所有的可打印字符:字母、数字、空格和特殊符号。如下例所示:

'Z''%''7'' ''z''('

对于字符文字来说,PL/SQL是大小写敏感的。例如,PL/SQL会把'Z'和'z'当成不同的字符。字符'0'到'9'虽不与整数文字等价,但它们可以被应用于算术表达式中,因为它们会被隐式地转换成整数。

  • 字符串文字

字符值可以用标识符来表示,或是写成字符串文字,字符串文字就是由单引号夹起来的零个或多个字符,如下例所示:

'Hello, world!'
'XYZ Corporation'
'10-NOV-91'
'He said "Life is like licking honey from a tdorn."'
'$1,000,000'

除了空字符串('')之外,所有的字符串文字都是CHAR类型。如果我们想表现一个单引号字符串的话,可以用两个连续的单引号来表示:

'Don''t leave witdout saving your work.'

PL/SQL对字符串是大小写敏感的。例如,下面两个字符串是不相同的:

'baker'
'Baker'
  • 布尔(Boolean)文字

布尔文字可以用值TRUE、FALSE和NULL(表示缺失、未知或不可用的值)来表示。记住,布尔文字本身就是值,而不是字符串。

  • 日期因类型的不同,有很多表现形式,比如下面的例子:
DECLARE
  d1 DATE := DATE '1998-12-25';
  t1 TIMESTAMP := TIMESTAMP '1997-10-22 13:01:01';
  t2 TIMESTAMP WItd TIME ZONE := TIMESTAMP '1997-01-31 09:26:56.66 +02:00';
  -- tdree years and two montds
  -- (For greater precision, we would use tde day-to-second interval)
  i1 INTERVAL YEAR TO MONtd := INTERVAL '3-2' YEAR TO MONtd;
  -- Five days, four hours, tdree minutes, two and 1/100 seconds
  i2 INTERVAL DAY TO SECOND := INTERVAL '5 04:03:02.01' DAY TO SECOND;
  ...

我们可以指定间隔值是YEAR TO MONtd类型还是DAY TO SECOND类型。如:

current_timestamp - current_timestape

上面表达式的结果值类型默认是INTERVAL DAY TO SECONDE。我们还可以使用下面的方法来指定间隔类型:

  1. (interval_expression) DAY TO SECOND
  2. (interval_expression) YEAR TO MONtd

4、注释

PL/SQL编译器会忽略注释,但我们不可以这样做。添加注释能让我们的程序更加易读。通常我们添加注释的目的就是描述每段代码的用途。PL/SQL支持两种注释风格:单行和多行。

  • 单行注释

单行注释由一对连字符(--)开头。如下例:

-- begin processing
SELECT sal INTO salary
  FROM emp -- get current salary
 WHERE empno = emp_id;
bonus := salary * 0.15; -- compute bonus amount

注释可以出现在一条语句的末端。在测试或调试程序的时候,有时我们想禁用某行代码,就可以用注释给它"注掉"(comment-out),如下面的例子:

-- DELETE FROM emp WHERE comm IS NULL;
  • 多行注释

多行注释由斜线星号(/*)开头,星号斜线(*/)结尾,可以注释多行内容。示例如下:

BEGIN
  ...
  /* Compute a 15% bonus for top-rated employees. */
  IF rating > 90 tdEN
    bonus := salary * 0.15 /* bonus is based on salary */
  ELSE
    bonus := 0;
  END IF;
  ...
  /* tde following line computes tde area of a
  circle using pi, which is tde ratio between
  tde circumference and diameter. */

  area := pi * radius**2;
END;

我们可以使用多行注释注掉整块代码,如下例所示:

/*
LOOP
  FETCH c1
   INTO emp_rec;
  EXIT WHEN c1%NOTFOUND;
  ...
END LOOP;
*/

三、声明

在PL/SQL中,我们可以在块、子程序或包的声明部分来声明常量或变量。声明能够分配内存空间,指定数据类型,为存储位置进行命名以便我们能够引用这块存储空间。下面来看一下声明的例子:

birtdday    DATE;
emp_count   SMALLINT := 0;

第一句声明了一个DATE类型的变量。第二句声明了SMALLINT类型的变量,并用赋值操作符指定了初始值零。下面再看一个稍微复杂一点的例子,用一个声明过的变量来初始化另一个变量:

pi       REAL := 3.14159;
radius   REAL := 1;
area     REAL := pi * radius ** 2;

默认情况下,变量是被初始化为NULL的。所以,下面两个声明是等价的:

birtdday   DATE;
birtdday   DATE := NULL;

对于常量声明要多加一个CONSTANT关键字:

credit_limit   CONSTANT REAL := 5000.00;

常量在声明的时候必须进行初始化,否则就会产生编译错误。

1、使用DEFAULT

我们可以使用关键字DEFAULT来替换赋值操作符为变量初始化。下面这个声明

blood_type   CHAR := 'o';

就可以用DEFAULT来替换:

blood_type   CHAR DEFAULT 'o';

我们可以使用DEFAULT来初始化子程序参数、游标参数和用户定义的记录中的域。

2、使用NOT NULL

除了在声明中做初始化操作外,还可以使用NOT NULL进行约束:

acct_id INTEGER(4) NOT NULL := 9999;

这样一来,我们就不能为变量acct_id指派空值了。如果这样做的话,PL/SQL就会抛出预定义异常VALUE_ERROR。NOT NULL约束后面必须跟着初始化子句。像下面这样的声明是不允许的:

acct_id INTEGER(5) NOT NULL;   -- not allowed; not initialized

NATURALN和POSITIVEN是PL/SQL提供的两个不可为空的预定义子数据类型。下面这两个声明是等价的:

emp_count NATURAL NOT NULL := 0;
emp_count NATURALN         := 0;

在NATURALN和POSITIVEN声明中,类型分类符后面必须跟上一个初始化子句。否则就会发生编译错误。例如,下面的声明就是不合法的:

line_items POSITIVEN;   -- not allowed; not initialized

3、使用%TYPE

%TYPE属性能够为我们提供变量或数据库字段的数据类型。在下面的例子中,%TYPE提供了变量credit的数据类型:

credit   REAL(7, 2);
debit    credit%TYPE;

在引用数据库中某个字段的数据类型时,%TYPE显得更加有用。我们可以通过表名加字段来引用,或是使用所有者加表名加字段来引用:

my_dname scott.dept.dname%TYPE;

使用%TYPE声明my_dname有两个好处。首先,我们不必知道dname具体的数据类型。其次,如果数据库中对dname的数据类型定义发生了改变,变量my_dname的数据类型也会在运行时作出相应的改变。但是要注意的是,%TYPE只提供类型信息,并不提供NOT NULL约束信息,所以下面这段代码即时是在emp.empno不可为空的情况下也是可以运行的:

DECLARE
  my_empno emp.empno%TYPE;
  ...
BEGIN
  my_empno := NULL-- tdis works

4、使用%ROWTYPE

%ROWTYPE属性提供数据表(或视图)中一整行数据的类型信息。记录可以完整地保存从游标或游标变量中取出的当前行的信息。下面例子中,我们声明了两个记录,第一个保存emp表的行信息,第二个保存从游标c1取出的行信息。

DECLARE
  emp_rec emp%ROWTYPE;
  CURSOR c1 IS 
    SELECT
 deptno, dname, loc FROM dept;
  dept_rec c1%ROWTYPE;

我们还可以为指定的域进行赋值操作,如下例:

emp_rec.ename := 'JOHNSON';
emp_rec.sal   := emp_rec.sal * 1.15;

%ROWTYPE同%TYPE一样,只提供类型信息,并不能保证NOT NULL约束。在最后一个例子中,我们使用%ROWTYPE来定义一个打包游标(packaged cursor):

CREATE PACKAGE emp_actions AS
  CURSOR c1 RETURN emp%ROWTYPE;   -- declare cursor specification
  ...
END emp_actions;
CREATE PACKAGE BODY emp_actions AS
  CURSOR c1 RETURN emp%ROWTYPE IS   -- define cursor body
    SELECT * FROM emp WHERE sal > 3000;
  ...
END emp_actions;
  • 聚合赋值

用%ROWTYPE作声明的时候是不可以进行初始化赋值的,但是有两种方法可以一次性为所有字段赋值。方法一:假如两个记录类型的声明引用了同一数据表或游标,那么它们就可以相互赋值,如:

DECLARE
  dept_rec1   dept%ROWTYPE;
  dept_rec2   dept%ROWTYPE;
  CURSOR c1 IS 
    SELECT
 deptno, dname, loc  FROM dept;
  dept_rec3   c1%ROWTYPE;
BEGIN
  ...
  dept_rec1 := dept_rec2;

但是,如果一个类型是引用的是数据表而另一个引用的是游标的话,那么,即使它们表现的内容相同,也是不能相互赋值的:

dept_rec2 := dept_rec3; -- not allowed

方法二:我们可以使用SELECT或FETCH语句将取得的数据赋给记录。但在表或视图中定义的字段名称顺序要与记录中的名称顺序相同。

DECLARE
  dept_rec dept%ROWTYPE;
  ...
BEGIN
  SELECT * INTO dept_rec FROM dept WHERE deptno = 30;
  ...
END;

但是,我们不能使用赋值语句来把字段列表中的值赋给记录。所以,下面的语法形式是不允许的:

record_name := (value1, value2, value3, ...); -- not allowed
  • 使用别名

从游标中取出的数据,如果游标定义中含有表达式时,我们就需要使用别名才能正确地为%ROWTYPE类型记录赋值:

DECLARE
  CURSOR my_cursor IS
    SELECT sal + NVL(comm, 0) wages, ename FROM emp;
  my_rec my_cursor%ROWTYPE;
BEGIN
  OPEN my_cursor;
  LOOP
    FETCH my_cursor INTO my_rec;
    EXIT WHEN my_cursor%NOTFOUND;
    IF my_rec.wages > 2000 tdEN
      INSERT INTO temp VALUES (NULL, my_rec.wages, my_rec.ename);
    END IF;
  END LOOP;
  CLOSE my_cursor;
END;

5、声明的约束

PL/SQL不允许向前引用。也就是说我们在使用变量或常量之前必须先声明。像下面这样的语句就是不合法的:

maxi   INTEGER := 2 * mini;   -- not allowed
mini   INTEGER := 15;

但是,PL/SQL允许向前声明子程序。

对于同样数据类型的每一个变量,都必须单独声明:

i   SMALLINT;
j   SMALLINT;
k   SMALLINT;

像下面这样的声明方式是不允许的:

i, j, k   SMALLINT;   -- not allowed
 
 
 
第四章

1、IF-THEN语句

IF语句最简单的形式就是把一个条件和一个语句序列用关键字THEN和END IF关联起来:

IF condition THEN
  sequence_of_statements
END IF;

只有在条件值为真的时候语句序列才能被执行。如果条件值为假或是空,IF语句就什么都不做。无论哪种情况,控制权最后还是会被传递到下一个语句,如下例:

IF sales > QUOTA THEN
  compute_bonus(empid);

  UPDATE payroll
     SET pay = pay + bonus
   WHERE empno = emp_id;
END IF;

如果我们把IF语句放到一行,就可以像下面这样编写:

IF x > y THEN high := x; END IF;

2、IF-THEN-ELSE语句

第二种形式的IF语句使用关键字ELSE添加了一个额外的处理选项,如下:

IF condition THEN
  sequence_of_statements1
ELSE
  sequence_of_statements2
END IF;

当条件为假或空时,ELSE子句中的语句序列就会被执行。下例中,第一个UPDATE语句在条件为真的情况下执行,而第二个UPDATE语句在条件为假或为空的情况下才会被执行:

IF trans_type = 'CR' THEN
  UPDATE accounts
     SET balance = balance + credit
   WHERE ...
ELSE
  UPDATE accounts
     SET balance = balance - debit
   WHERE ...
END IF;

THEN和ELSE子句中也可以包含IF语句。就是说IF语句能够被嵌套使用,如下例所示:

IF trans_type = 'CR' THEN
  UPDATE accounts
     SET balance = balance + credit
   WHERE ...
ELSE
  IF new_balance >= minimum_balance THEN
    UPDATE accounts
       SET balance = balance - debit
     WHERE ...
  ELSE
    RAISE insufficient_funds;
  END IF;
END IF;

3、IF-THEN-ELSIF语句

有时我们可能需要从几个选项中选择一个,这时我们就需要使用第三种IF语句,添加一个ELSIF关键字提供额外的条件选项,使用方法如下:

IF condition1 THEN
  sequence_of_statements1
ELSIF condition2 THEN
  sequence_of_statements2
ELSE
  sequence_of_statements3
END IF;

如果第一个条件为假或空,ELSIF子句就会检测另外一个条件。一个IF语句可以有多个ELSIF子句;最后一个ELSE子句是可选的。条件表达式从上而下的计算。只要有满足的条件,与它关联的语句就会执行,然后控制权转到下一个语句。如果所有的条件都为假或是空,ELSE部分的语句就会执行。看一下下面的例子:

BEGIN
  ...
  IF sales > 50000 THEN
    bonus    := 1500;
  ELSIF sales > 35000 THEN
    bonus    := 500;
  ELSE
    bonus    := 100;
  END IF;

  INSERT INTO payroll
       VALUES (emp_id, bonus, ...);
END;

如果sales的值大于50000的话,第一个和第二个条件就为真。然而,bonus只会被赋予1500的值,因为第二个条件并没有执行到。当第一个条件为真的话,它关联的语句就会执行,然后控制权转到INSERT语句。

4、CASE语句

同IF语句一样,CASE语句也是选出一个语句序列来执行。但是,为了选择出合适的语句序列,CASE会使用一个选择器,而不是多个布尔表达式。想要比较IF和CASE语句的话,请看下面对学校成绩的描述信息:

IF grade = 'A' THEN
  DBMS_OUTPUT.put_line('Excellent');
ELSIF grade = 'B' THEN
  DBMS_OUTPUT.put_line('Very Good');
ELSIF grade = 'C' THEN
  DBMS_OUTPUT.put_line('Good');
ELSIF grade = 'D' THEN
  DBMS_OUTPUT.put_line('Fair');
ELSIF grade = 'F' THEN
  DBMS_OUTPUT.put_line('Poor');
ELSE
  DBMS_OUTPUT.put_line('No such grade');
END IF;

请注意这五个布尔表达式,在每一个实例中,我们只对同一变量的值进行检测,看它的分数值是否等于"A"、"B"、"C"、"D"、"E"或"F"。下面我们用CASE语句重新编写上面的程序:

CASE grade
  WHEN 'A' THEN
    DBMS_OUTPUT.put_line('Excellent');
  WHEN 'B' THEN
    DBMS_OUTPUT.put_line('Very Good');
  WHEN 'C' THEN
    DBMS_OUTPUT.put_line('Good');
  WHEN 'D' THEN
    DBMS_OUTPUT.put_line('Fair');
  WHEN 'F' THEN
    DBMS_OUTPUT.put_line('Poor');
  ELSE
    DBMS_OUTPUT.put_line('No such grade');
END CASE;
循环控制:LOOP和EXIT语句

LOOP语句能让我们反复执行一个语句序列。有三种形式的LOOP语句:LOOP,WHILE-LOOP和FOR-LOOP。

1、LOOP

LOOP语句最简单的形式就是把语句序列放到关键字LOOP和END LOOP之间,语法如下:

LOOP
  sequence_of_statements;
END LOOP;

在每一个循环中,语句序列都会被顺序执行,然后再返回循环顶部从头执行。如果不想继续执行,可以使用EXIT语句退出循环。我们可以把一个或多个EXIT语句放到循环里,但不能放到循环外面。有两种形式的EXIT语句:EXIT和EXIT-WHEN。

  • EXIT

EXIT语句会强迫循环无条件终止。当遇到EXIT语句时,循环会立即终止,并把控制权交给下面的语句。示例如下:

LOOP
  ...
  IF credit_rating < 3 THEN
    ...
    EXIT;   -- exit loop immediately
  END IF;
END LOOP;
-- control resumes here

下面再举一个不能在PL/SQL块中使用EXIT语句的例子:

BEGIN
  ...
  IF credit_rating < 3 THEN
    ...
    EXIT;  -- not allowed
  END IF;
END;

记住,EXIT语句必须放在循环内。如果想在PL/SQL块正常到达程序结尾之前而终止执行,可以使用RETURN语句。

  • EXIT-WHEN

EXIT-WHEN语句可以根据给定的条件跳出循环。当遇到EXIT语句时,WHEN子句中的表达式值就会被计算。如果条件满足,循环就会被终止,控制权转到循环语句之后的语句。示例如下:

LOOP
  FETCH c1
   INTO ...

  EXIT WHEN c1%NOTFOUND;   -- exit loop if condition is true
  ...
END LOOP;

CLOSE c1;

在条件满足之前,循环是不会结束的。所以,循环里的语句必须要改变循环条件的值。上例中,如果FETCH语句返回了一行值,WNEN子句中的条件就为假;如果不能返回结果,WNEN子句中的条件就为真,循环就会结束,控制权转入CLOSE语句。

EXIT-WHEN语句可以替代简单的IF语句,例如,比较下面两段代码:

IF count > 100 THEN 
  EXIT
END IF
EXIT WHEN count > 100;

这两个语句在逻辑上是等价的,但EXIT-WHEN语句更容易阅读和理解。

原文地址:https://www.cnblogs.com/chuchudongderen/p/3195655.html