24.2. 描述

24.2.1. PL/pgSQL 的结构

PL/pgSQL 是一种块结构的语言. 所有关键字和标识都可以混合大小写使用. 一个块象下面这样定义∶

[<<label>>]
[DECLARE
    declarations]
BEGIN
    statements
END;

在一个语句段(statement section)里面可以有任意个子语句块. 子语句块可以用于屏蔽语句块外面的变量.

在语句块前面的声明段(declarations section)里定义的变量在每次进入 语句块时都初始化为它们的缺省值,而不是每次函数调用时初始化一次. 比如∶

CREATE FUNCTION somefunc() RETURNS INTEGER AS '
DECLARE
   quantity INTEGER := 30;
BEGIN
   RAISE NOTICE ''Quantity here is %'',quantity;  -- Quantity here is 30
   quantity := 50;
   --
   -- Create a sub-block
   --
   DECLARE
      quantity INTEGER := 80;
   BEGIN
      RAISE NOTICE ''Quantity here is %'',quantity;  -- Quantity here is 80
   END;

   RAISE NOTICE ''Quantity here is %'',quantity;  -- Quantity here is 50
END;
' LANGUAGE 'plpgsql';

我们一定不要把 PL/pgSQL 里用于语句分组的 BEGIN/END 和 用于事务控制的数据库命令搞混了. PL/pgSQL 的 BEGIN/END 只是用于分组(译注∶象 C 里的 {}); 它们不会开始和结束一个事务. 函数和触发器过程总是在一个由外层查询建立起来的事务里执行 --- 它们无法开始或者提交事务,因为 Postgres 没有嵌套事务.

24.2.2. 注释

在 PL/pgSQL 里有两种类型的注释.双破折号 -- 引出到该行行尾的单行注释. 一个 /* 开始一个块注释,一直延伸到下一个 */出现. 块注释不能嵌套使用, 但是双破折号注释可以封装在一个块注释里面并且双破折号注释可以隐藏一个块 注释分隔符 /**/

24.2.3. 变量和内容

所有在块或者块的子块里使用的变量,行和记录 都必须在一个块的声明段里声明.唯一的例外是 一个 FOR 循环里的循环变量是在一个整数范围内迭代的.

PL/pgSQL 变量可以由任意的 SQL 数据类型,比如 INTEGERVARCHARCHAR.所有变量的缺省值都是 SQL NULL 数值.

下面是一些变量声明的例子∶

user_id INTEGER;
quantity NUMBER(5);
url VARCHAR;

24.2.3.1. 带缺省值的常量和变量

声明有下面的语法∶

name [ CONSTANT ] type [ NOT NULL ] [ { DEFAULT | := } value ];

定义为 CONSTANT 的变量数值不能修改.如果声明了 NOT NULL,那么赋予 NULL 数值将导致一个运行时错误.因为所有变量的缺省值是 SQL NULL,所以所有声明为 NOT NULL 的变量还必须 声明一个缺省值.

缺省值是在每次调用函数的时候计算的.因此给一个类型为 timestamp 的变量赋值会令变量拥有函数实际调用 的时间,而不是函数预编译成其字节码的时间.

例子∶

quantity INTEGER := 32;
url varchar := ''http://mysite.com'';
user_id CONSTANT INTEGER := 10;

24.2.3.2. 传递给函数的变量

传递给函数的变量都是用 $1$2, 等等这样的标识符(最大是 16)命名的.一些例子∶

CREATE FUNCTION sales_tax(REAL) RETURNS REAL AS '
DECLARE
    subtotal ALIAS FOR $1;
BEGIN
    return subtotal * 0.06;
END;
' LANGUAGE 'plpgsql';


CREATE FUNCTION instr(VARCHAR,INTEGER) RETURNS INTEGER AS '
DECLARE
    v_string ALIAS FOR $1;
    index ALIAS FOR $2;
BEGIN
    -- Some computations here
END;
' LANGUAGE 'plpgsql';

24.2.3.3. 属性

使用 %TYPE%ROWTYPE 属性, 你可以声明与另外一个数据库项相同的数据类型或结构(比如∶ 一个表的域).

%TYPE

%TYPE 给我们提供了一个变量或者数据库列的数据类型. 你可以用这个关键字声明那些需要保存数据库数值的变量. 举例来说,让我们假设你有一个叫 user_id 的列 在你的 users 表里.要声明一个和它相同的数据类型, 你可以∶

user_id users.user_id%TYPE;

通过使用 %TYPE,你就不需要知道你引用的 结构的数据类型,最重要的是,如果被引用项的数据类型在将来 变化了(比如你把表定义列的 user_id 改成 REAL),那么你就 不用修改你的函数定义.

name table%ROWTYPE;

声明一个与给定表结构相同的行.table 必须 是该数据库列一个现存的表或者视图.该行的域是用点表示法进行访问的. 给函数的参数可以是组合类型(完整的表的行).在这种情况下,对应的 标识符 $n 就是一个行类型,但是你必须用上面描述的 ALIAS 命令给它取 一个别名.

只有表里面的用户属性是可以访问的,而 OID 或者其它系统属性(因为 该行可以来自一个视图)则不能访问.行类型的字段为char() 或类似的类型继承表的字段大小或精度.

DECLARE
    users_rec users%ROWTYPE;
  user_id users%TYPE;
BEGIN
    user_id := users_rec.user_id;
    ...

create function cs_refresh_one_mv(integer) returns integer as '
   DECLARE
        key ALIAS FOR $1;
        table_data cs_materialized_views%ROWTYPE;
   BEGIN
        SELECT INTO table_data * FROM cs_materialized_views
               WHERE sort_key=key;

        IF NOT FOUND THEN
           RAISE EXCEPTION ''View '' || key || '' not found'';
           RETURN 0;
        END IF;

        -- The mv_name column of cs_materialized_views stores view
        -- names.
 
        TRUNCATE TABLE table_data.mv_name;
        INSERT INTO table_data.mv_name || '' '' || table_data.mv_query;

        return 1;
end;
' LANGUAGE 'plpgsql';

24.2.3.4. RENAME

你可以用 RENAME 修改一个变量,记录或者行的名字. 如果 NEW 或者 OLD 在个触发器过程里被另外一个名字引用, 那么这个东西就很有用.

语法和例子∶

RENAME oldname TO newname;

RENAME id TO user_id;
RENAME this_var TO that_var;

24.2.4. 表达式

所有在 PL/pgSQL 语句里使用的表达式都是用后端的执行器进行处理的. 那些包含常量的表达式实际上需要运行时计算(比如,用于 timestamp 类型的 'now'),因此 除了 NULL 关键字之外,对于 PL/pgSQL 分析器而言,实际上是不可能 标识真正的常量数值的.所有表达式都是使用 SPI 管理器通过在内部执行一个查询计算的.

SELECT expression
在表达式里,出现标识符的地方被那些从参数数组里传递给执行器的变量的实际 数值替代. 所有在 PL/pgSQL 函数里使用的表达式都只准备和保存一次. 唯一的例外是 EXECUTE 语句,每次运行到 EXECUTE 的时候, 如果需要分析查询,那么都会对其进行分析.

Postgres 的主分析器做的类型检查 对常量数值的代换有一些副作用.详细说来就是下面这两个函数 做的事情有些区别∶

CREATE FUNCTION logfunc1 (text) RETURNS timestamp AS '
    DECLARE
        logtxt ALIAS FOR $1;
    BEGIN
        INSERT INTO logtable VALUES (logtxt, ''now'');
        RETURN ''now'';
    END;
' LANGUAGE 'plpgsql';
CREATE FUNCTION logfunc2 (text) RETURNS timestamp AS '
    DECLARE
        logtxt ALIAS FOR $1;
        curtime timestamp;
    BEGIN
        curtime := ''now'';
        INSERT INTO logtable VALUES (logtxt, curtime);
        RETURN curtime;
    END;
' LANGUAGE 'plpgsql';
logfunc1() 的实例里, Postgres 的主分析器在 为 INSERT 准备查询规划的时候知道字串 'now' 应该解释成 timestamp 类型,因为 logtable 的目标 列就是该类型.所以,它会在这个时候从这个字串中计算一个常量, 然后在该后端的整个生存期中的所有 logfunc1() 调用中使用这个常量.不消说,这可不是程序员想要的.

logfunc2()里, Postgres 的主分析器并不知道 'now' 应该转换成什么类型, 因此它返回一个包含字符串 'now'text 数据类型. 在给局部变量 curtime 赋值时,PL/pgSQL 解释器通过调用 text_out()timestamp_in() 把这个字符串转换成 timestamp 类型的变量.

这个 Postgres 主分析器的类型检查是在 PL/pgSQL 接近完成的时候实现的. 它是版本 6.3 和版本 6.4 之间的差别并且影响所有使用 SPI 管理器的规划准备特性的函数. 使用上面提到的局部变量的方法是目前能让 PL/pgSQL 对那些数值正确解释的唯一方法.

如果在表达式或语句里用到记录(record)字段, 字段的数据类型在同一个表达式的不同调用中不应该改变. 书写控制来自多个表事件的触发器过程时应该注意这一点.

24.2.5. 语句

任何象下面声明的 PL/pgSQL 分析器不能理解的东西将 被放到查询里并发送给数据库引擎执行.生成的查询应该不返回任何数据.

24.2.5.1. 赋值

给一个变量或行/记录赋值用下面方法∶

identifier := expression;
如果表达式的结果数据类型和变量数据类型不一致, 或者变量具有已知的尺寸/精度(例如 char(20)), 结果值将隐含地被 PL/pgSQL 字节码解释器用结果类型的输出函数 和变量类型的输入函数转换. 要注意这样做可能潜在地导致类型输入函数生成的运行时错误.

user_id := 20;
tax := subtotal * 0.06;

24.2.5.2. 调用其他函数

所有在 Prostgres 数据库里的函数返回一个值. 因此,通常的调用函数的方法是执行一条 SELECT 查询或者做一个赋值 (导致一个 PL/pgSQL 的内部 SELECT ).

但是有时候我们对函数结果并不感兴趣. 这种时候,使用 PERFORM 语句.

PERFORM query
将在 SPI 管理器上执行一个 'SELECT query' 然后丢弃结果.象局部变量这样的标识仍然代入参数.

PERFORM create_mv(''cs_session_page_requests_mv'',''
     select   session_id, page_id, count(*) as n_hits,
              sum(dwell_time) as dwell_time, count(dwell_time) as dwell_count
     from     cs_fact_table
     group by session_id, page_id '');

24.2.5.3. 执行动态查询

你经常会希望在你的 PL/pgSQL 函数里生成动态查询. 或者你有一些会生成其它函数的函数.PL/pgSQL 为这样的 场合提供 EXECUTE 语句.

EXECUTE query-string
这里的 query-string 是一个类型为 text 的字串,包含要执行的 查询(query)

在使用动态查询的时候,你就必须面对 PL/pgSQL 的单引号 逃逸的问题.请参考"从 Oracle PL/SQL 移植"一章里的表 获取详细的解释,这些说明可以帮你省不少功夫.

和所有其它在 PL/pgSQL 里的查询不同,一个由 EXECUTE 语句 运行的 query 在服务器生命期内 并不只准备和保存一次.相反,在该语句每次运行的时候, query 都准备一次. query-string 可以在过程里动态地 生成以便于对各种不同的表和字段进行操作.

来自 SELECT 查询的结果被 EXECUTE 抛弃,并且目前 EXECUTE 里面还不支持 SELECT INTO.因此,从一个动态创建的 SELECT 里抽取结果的唯一方法是使用我们稍后描述的 FOR ... EXECUTE 语句.

一个例子∶

EXECUTE ''UPDATE tbl SET ''
        || quote_ident(fieldname)
        || '' = ''
        || quote_literal(newvalue)
        || '' WHERE ...'';

这个例子显示了函数 quote_ident(TEXT) 和 quote_literal(TEXT) 的使用. 包含字段和表标识符的变量应该传递给函数 quote_ident(). 包含动态查询字串的文本元素的变量应该传递给 quote_literal().它们俩都会采取合适的 步骤把输入文本包围在单或双引号里以及用任何嵌入的特殊字符返回.

下面是一个大得多的动态查询和 EXECUTE 的例子∶

CREATE FUNCTION cs_update_referrer_type_proc() RETURNS INTEGER AS '
DECLARE
    referrer_keys RECORD;  -- 声明一个用于 FOR 里的一个通用的记录
    a_output varchar(4000);
BEGIN 
    a_output := ''CREATE FUNCTION cs_find_referrer_type(varchar,varchar,varchar) 
                  RETURNS varchar AS '''' 
                     DECLARE 
                         v_host ALIAS FOR $1; 
                         v_domain ALIAS FOR $2; 
                         v_url ALIAS FOR $3; ''; 

    -- 
    -- 请注意这里是如何在一个 FOR 循环中扫描所有的结果的
    -- 这里用的是 FOR <record> 构造
    --

    FOR referrer_keys IN select * from cs_referrer_keys order by try_order LOOP
        a_output := a_output || '' if v_'' || referrer_keys.kind || '' like '''''''''' 
                 || referrer_keys.key_string || '''''''''' then return '''''' 
                 || referrer_keys.referrer_type || ''''''; end if;''; 
    END LOOP; 
  
    a_output := a_output || '' return null; end; '''' language ''''plpgsql'''';''; 
 
    -- 我们可以象下面这样写是因为我们没有替换任何变量
    -- 否则它会出错.看看 PERFORM 获取另外一种运行函数的方法
    
    EXECUTE a_output; 
end; 
' LANGUAGE 'plpgsql';

24.2.5.4. 获取其它结果状态

GET DIAGNOSTICS variable = item [ , ... ]
这条命令允许我们检索系统状态标识符.每个 item 是一个关键字,表示一个将要赋予该特定变量的状态值(该变量应该 和要接收的数值类型相同).当前可用的状态项有 ROW_COUNT, 最后一个发送给 SQL 引擎的 SQL 查询处理的行的数量;和 RESULT_OID,最后一条 SQL 查询插入的最后一行的 Oid.请注意 RESULT_OID 只有在一个 INSERT 查询之后才有用.

24.2.5.5. 从函数返回

RETURN expression
函数结束并且表达式 expression 的值将返回给上层执行器. 函数的返回值不能是无定义.如果控制到达一个函数体的最顶层而 没有碰到一个 RETURN 语句,将产生一个运行时错误.

表达式的结果将被自动转换成函数返回类型--象我们在赋值里描述的那样.

24.2.6. 控制结构

控制结构可能是 PL/SQL 里最有用(以及最重要)的部分. 利用 PL/pgSQL 的控制结构,你可以以非常灵活很功能强大 的方法操纵 PostgreSQL 数据.

24.2.6.1. 条件控制∶ IF 语句

IF 语句让你可以根据某种条件采取动作. PL/pgSQL 有三种形式的 IF∶IF-THEN,IF-THEN-ELSE,IF-THEN-ELSE IF. 注意∶所有 PL/pgSQL 的 IF 语句需要一个对应的 END IF 语句.在 ELSE-IF 语句里,你需要两个∶一个用于第一个 IF 而另外一个 用于第二个(ELSE IF).

IF-THEN

IF-THEN 语句是 IF 的最简单形式.如果条件为真, 在 THEN 和 END IF 之间的语句将被执行. 否则,将执行跟在 END IF 后面的语句.

IF v_user_id <> 0 THEN
    UPDATE users SET email = v_email WHERE user_id = v_user_id;
END IF;

IF-THEN-ELSE

IF-THEN-ELSE 语句增加了 IF-THEN 的分支,让你可以声明 在条件计算为 FALSE 的时候执行的语句.

IF parentid IS NULL or parentid = ''''
THEN 
    return fullname;
ELSE
    return hp_true_filename(parentid) || ''/'' || fullname;
END IF;


IF v_count > 0 THEN 
    INSERT INTO users_count(count) VALUES(v_count);
    return ''t'';
ELSE 
    return ''f'';
END IF;

IF 语句可以嵌套并且在下面的例子中∶

IF demo_row.sex = ''m'' THEN
  pretty_sex := ''man'';
ELSE
  IF demo_row.sex = ''f'' THEN
    pretty_sex := ''woman'';
  END IF;
END IF;

IF-THEN-ELSE IF

如果你使用 "ELSE IF" 语句,那么你实际上就是在 ELSE 语句中嵌套了一个 IF 语句.因此你需要一个 END IF 语句 给每个嵌套的 IF,另外还要一个给父 IF-ELSE 用.

比如∶

IF demo_row.sex = ''m'' THEN
   pretty_sex := ''man'';
ELSE IF demo_row.sex = ''f'' THEN
        pretty_sex := ''woman'';
     END IF;
END IF;

24.2.6.2. 迭代控制∶ LOOP, WHILE, FOR 和 EXIT

使用 LOOP,WHILE,FOR 和 EXIT 语句,你可以控制你的 PL/pgSQL 程序迭代的流程.

LOOP

[<<label>>]
LOOP
    statements
END LOOP;
一个无条件的循环,你必须用 EXIT 语句明确退出.可选的标签 可以由 EXIT 语句使用,用在嵌套循环中声明应该结束哪一层 循环.

EXIT

EXIT [ label ] [ WHEN expression ];
如果没有给出 label, 那么退出最内层的循环,然后执行跟在 END LOOP 后面的语句. 如果给出 label, 那么它必须是当前或者更高层的嵌套循环块的标签. 然后该命名块或者循环就会终止,而控制落到对应循环/块的 END 语句后面的语句上.

例子∶

LOOP
    -- 一些计算
    IF count > 0 THEN
        EXIT;  -- exit loop
    END IF;
END LOOP;

LOOP
    -- 一些计算
    EXIT WHEN count > 0;
END LOOP;

BEGIN
    -- 一些计算
    IF stocks > 100000 THEN
        EXIT;  -- 非法,不能用 EXIT 退出到 LOOP 外面.
    END IF;
END;

WHILE

利用 WHILE 语句,只要条件表达式为真,那么你就可以 不停在一系列语句上进行循环.

[<<label>>]
WHILE expression LOOP
    statements
END LOOP;
比如∶
WHILE amount_owed > 0 AND gift_certificate_balance > 0 LOOP
    -- 可以在这里做些计算
END LOOP;

WHILE NOT boolean_expression LOOP
    -- 可以在这里做些计算
END LOOP;

FOR

[<<label>>]
FOR name IN [ REVERSE ] expression .. expression LOOP
    statements
END LOOP;
一个对一定范围的整数数值进行迭代的循环.变量 name 会自动作为整数类型创建并且只在 循环里存在.给出范围的上下界的两个表达式知识在进入循环的时候才计算. 迭代步进值总是为一.

一些 FOR 循环的例子(参阅Section 24.2.7 获取在 FOR 循环中迭代所有记录的例子)∶

FOR i IN 1..10 LOOP
  -- 这里可以放一些表达式

    RAISE NOTICE 'i is %',i;
END LOOP;

FOR i IN REVERSE 1..10 LOOP
    -- 这里可以放一些表达式
END LOOP;

24.2.7. 使用 RECORD

记录(record)类似行类型,不过它们没有预定义的结构. 它们用于选择查询和 FOR 循环中保留来自 SELECT 操作的一个实际的行.

24.2.7.1. 声明

一个类型为 RECORD 的变量可以用于不同的选择. 如果一个记录变量里还没有实际的数据行的时候, 访问一条记录或者试图给一个记录字段赋值的动作都会 导致一个运行时错误.它们可以象这样声明∶

name RECORD;

24.2.7.2. 赋值

一次完整的选择的结果赋予一个记录或者一行的做法可以是∶

SELECT  INTO target expressions FROM ...;
target 可以是一条记录,一个行变量 或者一个逗号分隔的变量列表,还有记录-/行-字段.请注意这里 和 Postgres 里普通的 SELECT INTO 的含义是不同的,在普通的 SELECT INTO 里,INTO 的目标是一个新创建的表.(如果你想在 PL/pgSQL 函数里从一个 SELECT 的结果中创建一个表,那么请使用 等效的 CREATE TABLE AS SELECT 语法.)

如果把一行或者一个变量列表用做目标,那么选出的数值必须准确地 匹配目标的结构,否则就会产生一个运行时错误.FROM 关键字后面可以 跟着人和有效的条件,分组,排序等等可以给 SELECT 语句的东西.

一旦将一个记录或者一行赋予了一个 RECORD 变量, 那么你就可以使用"."(点)表示法访问该记录中的字段∶

DECLARE
    users_rec RECORD;
    full_name varchar;
BEGIN
    SELECT INTO users_rec * FROM users WHERE user_id=3;

  full_name := users_rec.first_name || '' '' || users_rec.last_name;

有一个类型为 boolean 的特殊变量 FOUND, 你可以用它紧跟在 SELECT INTO 后面检查一次赋值是否成功.

SELECT INTO myrec * FROM EMP WHERE empname = myname;
IF NOT FOUND THEN
    RAISE EXCEPTION ''employee % not found'', myname;
END IF;
你还可以用 IS NULL (或者 ISNULL) 条件测试一个 RECORD/ROW 是否为空.如果该查询返回多个行,那么只有第一行移动到目标字段 中.所有其它的都被不声不响地干掉了.

DECLARE
    users_rec RECORD;
    full_name varchar;
BEGIN
    SELECT INTO users_rec * FROM users WHERE user_id=3;

    IF users_rec.homepage IS NULL THEN
        -- user entered no homepage, return "http://"

        return ''http://'';
    END IF;
END;

24.2.7.3. 遍历所有的记录

利用 FOR 循环的一种特殊类型,你可以遍历一个查询 的记录并且相应处理那些书局.语法如下∶

[<<label>>]
FOR record | row IN select_clause LOOP
    statements
END LOOP;
这里的 record 或者 row 将被赋予从 select 子句来的所有行, 并且为每一行执行循环体.下面是一个例子∶

create function cs_refresh_mviews () returns integer as '
DECLARE
     mviews RECORD;

     -- 如果你用下面这样的方法声明∶
     -- mviews  cs_materialized_views%ROWTYPE;
     -- 那么这个 record 就*只能*用于 cs_materialized_view 表了

BEGIN
     PERFORM cs_log(''Refreshing materialized views...'');

     FOR mviews IN SELECT * FROM cs_materialized_views ORDER BY sort_key LOOP

         -- 现在 "mviews" 里有了一条来自 cs_materialized_views 的记录

         PERFORM cs_log(''Refreshing materialized view '' || mview.mv_name || ''...'');
         TRUNCATE TABLE mview.mv_name;
         INSERT INTO mview.mv_name || '' '' || mview.mv_query;
     END LOOP;

     PERFORM cs_log(''Done refreshing materialized views.'');
     return 1;
end;
' language 'plpgsql';
如果循环是用一个 EXIT 语句终止的,那么在循环之后你仍然可以 访问最后赋值的行.

FOR-IN EXECUTE 语句是遍历所有记录的另外一种方法∶

[<<label>>]
FOR record | row IN EXECUTE text_expression LOOP 
    statements
END LOOP;
这个例子类似前面的形式,只不过源 SELECT 语句声明为了一个 字串表达式,这样它在每次进入 FOR 循环的时候都会重新计算和 生成查询规划.这样就允许程序员在一个预先规划好了的查询所获得 的速度和一个动态查询所获得的灵活性(就象一个简单的 EXECUTE 语句那样)之间进行选择.

24.2.8. 退出和消息

利用 RAISE 语句抛出一个消息到 Postgres elog 机制中.

RAISE level 'format' [, identifier [...]];
在格式里,% 是用做随后跟着的逗号分隔的标识符 的占位符的.可能的级别是 DEBUG(在运行的生产库中会被屏蔽掉),NOTICE (写入数据库日志并转发给客户端应用)和 EXCEPTION(写到数据库日志并且 退出事务).

RAISE NOTICE ''Id number '' || key || '' not found!'';
RAISE NOTICE ''Calling cs_create_job(%)'',v_job_id;
在这最后一个例子里,v_job_id 将被字串中的 % 代替.

RAISE EXCEPTION ''Inexistent ID --> %'',user_id;
这样会退出事务并且写到数据库日志里.

24.2.9. 例外处理

Postgres 现在还没有足够聪明的 例外处理模型.不管是分析器,规划器/优化器还是执行器认为 一条语句无法再处理下去了,那么整个事务都退出,然后系统跳回 主循环等待来自客户端应用的下一条查询.

我们可以'钩'在错误处理机制上来提示这种情况的发生. 但是目前我们没有能力告诉(用户)是什么导致了退出(输入/输出转换错误, 浮点数错误,分析错误).并且此时的数据库后端可能处在一种不连贯的状态, 所以退回到上层执行器或执行更多的命令可能摧毁整个数据库. 而且此时事务退出的信息可能已经发送给了客户端应用, 所以继续操作没有任何意义.

因此,PL/pgSQL 在函数或触发器操作时遇到退出的唯一一项操作是在 DEBUG 级别运行时输出一些附加的日志信息, 报告在哪个函数和在那里(行号和语句类型)出了错.