Delphi 之 第六課 過程與函數

  這講是核心重點*****

 
  什麼是過程?什麼又是函數?過程和函數在delphi中無處不再。過程簡單的理解就是單擊一個按鈕這就是一個過程,函數和過程不一樣的地方就是函數能把這個過程的結果返回給我們,過程的關鍵字用procedure ,函數的關鍵字Function 

下面就具體講解過程與函數的定義

  過程與函數

  Pascal中的例程有兩種形式:過程和函數。理論上說,過程是你要求計算機執行的操作,函數是能返回值的計算。兩者突出的不同點在於:函數能返回計算結果,即有一個返回值,而過程沒有。兩種類型的例程都可以帶多個給定類型的參數。

  不過實際上函數和過程差別不大,因爲你可以調用函數完成一系列操作,跳過其返回值(用可選的出錯代碼或類似的東西代替返回值);也可以通過過程的參數傳遞計算結果(這種參數稱爲引用,下一部分會講到)。

下例定義了一個過程、兩個函數,兩個函數的語法略有不同,結果是完全相同的。

procedure Hello;
begin
  ShowMessage ('Hello world!');
end;

function Double (Value: Integer) : Integer;
begin
  Double := Value * 2;
end;

// or, as an alternative
function Double2 (Value: Integer) : Integer;
begin
  Result := Value * 2;
end;

  流行的做法是用Result 給函數賦返回值,而不是用函數名,我認爲這樣的代碼更易讀。

一旦定義了這些例程,你就可以多次調用,其中調用過程可執行操作;調用函數能計算返回值。如下:

procedure TForm1.Button1Click (Sender: TObject);
begin
  Hello;
end;
 
procedure TForm1.Button2Click (Sender: TObject);
var
  X, Y: Integer;
begin
  X := Double (StrToInt (Edit1.Text));
  Y := Double (X);
  ShowMessage (IntToStr (Y));
end;

注意:現在不必考慮上面兩個過程的語法,實際上它們是方法。只要把兩個按鈕(button)放到一個Delphi 窗體上,在設計階段單擊它們,Delphi IDE將產生合適的支持代碼,你只需要填上begin 和end 之間的那幾行代碼就行。編譯上面的代碼,需要你在窗體中加一個Edit控件。

  現在回到我前面提到過的代碼封裝概念。當你調用Double 函數時,你不需要知道該函數的具體實現方法。如果以後發現了更好的雙倍數計算方法,你只需要改變函數的代碼,而調用函數的代碼不必改變(儘管代碼執行速度可能會加快!)。Hello 過程也一樣,你可以通過改變這個過程的代碼,修改程序的輸出,Button2Click 方法會自動改變顯示結果。下面是改變後的代碼:

procedure Hello;
begin
  MessageDlg ('Hello world!', mtInformation, [mbOK],0);
end;

提示:當調用一個現有的Delphi 函數、過程或任何VCL方法時,你應該記住參數的個數及其數據類型。不過,只要鍵入函數或過程名及左括號,Delphi 編輯器中會出現即時提示條,列出函數或過程的參數表供參考。這一特性被稱爲代碼參數(Code Parameters) ,是代碼識別技術的一部分。

引用參數

  Pascal 例程的傳遞參數可以是值參也可以是引用參數。值參傳遞是缺省的參數傳遞方式:即將值參的拷貝壓入棧中,例程使用、操縱的是棧中的拷貝值,不是原始值。

  當通過引用傳遞參數時,沒有按正常方式把參數值的拷貝壓棧(避免拷貝值壓棧一般能加快程序執行速度),而是直接引用參數原始值,例程中的代碼也同樣訪問原始值,這樣就能在過程或函數中改變參數的值。引用參數用關鍵字var 標示。

  參數引用技術在大多數編程語言中都有,C語言中雖沒有,但C++中引入了該技術。在C++中,用符號 &表示引用;在VB中,沒有ByVal 標示的參數都爲引用。

  下面是利用引用傳遞參數的例子,引用參數用var關鍵字標示:

procedure DoubleTheValue (var Value: Integer);
begin
  Value := Value * 2;
end;

  在這種情況下,參數既把一個值傳遞給過程,又把新值返回給調用過程的代碼。當你執行完以下代碼時:

var
  X: Integer;
begin
  X := 10;
  DoubleTheValue (X);

  x變量的值變成了20,因爲過程通過引用訪問了X的原始存儲單元,由此改變了X的初始值。

  通過引用傳遞參數對有序類型、傳統字符串類型及大型記錄類型纔有意義。實際上Delphi總是通過值來傳遞對象,因爲Delphi對象本身就是引用。因此通過引用傳遞對象就沒什麼意義(除了極特殊的情況),因爲這樣相當於傳遞一個引用到另一個引用。

  Delphi 長字符串的情況略有不同,長字符串看起來象引用,但是如果你改變了該字符串的串變量,那麼這個串在更新前將被拷貝下來。作爲值參被傳遞的長字符串只在內存使用和操作速度方面才象引用,但是如果你改變了字符串的值,初始值將不受影響。相反,如果通過引用傳遞長字符串,那麼串的初始值就可以改變。

  Delphi 3增加了一種新的參數:out。out參數沒有初始值,只是用來返回一個值。out參數應只用於COM過程和函數,一般情況下最好使用更有效的var參數。除了沒有初始值這一點之外,out參數與var參數相同。

常量參數

  除了引用參數外,還有一種參數叫常量參數。由於不允許在例程中給常量參數賦新值,因此編譯器能優化常參的傳遞過程。編譯器會選用一種與引用參數相似的方法編譯常參(C++術語中的常量引用),但是從表面上看常參又與值參相似,因爲常參初始值不受例程的影響。

  事實上,如果編譯下面有點可笑的代碼,Delphi將出現錯誤:

function DoubleTheValue (const Value: Integer): Integer;
begin
  Value := Value * 2;      // compiler error
  Result := Value;
end;

開放數組參數

  與C語言不同,Pascal 函數及過程的參數個數是預定的。如果參數個數預先沒有確定,則需要通過開放數組來實現參數傳遞。

一個開放數組參數就是一個固定類型開放數組的元素。 也就是說,參數類型已定義,但是數組中的元素個數是未知數。見下例:

function Sum (const A: array of Integer): Integer;
var
  I: Integer;
begin
  Result := 0;
  for I := Low(A) to High(A) do
    Result := Result + A[I];
end;

  上面通過High(A)獲取數組的大小,注意其中函數返回值 Result的應用, Result用來存儲臨時值。你可通過一個整數表達式組成的數組來調用該函數:

X := Sum ([10, Y, 27*I]);

  給定一個整型數組,數組大小任意,你可以直接把它傳遞給帶開放數組參數的例程,此外你也可以通過Slice 函數,只傳遞數組的一部分元素(傳遞元素個數由Slice 函數的第二個參數指定)。下面是傳遞整個數組參數的例子:

var
  List: array [1..10] of Integer;
  X, I: Integer;
begin
  // initialize the array
  for I := Low (List) to High (List) do
    List [I] := I * 2;
  // call
  X := Sum (List);

  如果你只傳遞數組的一部分,可使用Slice 函數,如下:

X := Sum (Slice (List, 5));

 

在Delphi 4中,給定類型的開放數組與動態數組完全兼容(動態數組將在第8章中介紹)。動態數組的語法與開放數組相同,區別在於你可以用諸如array of Integer指令定義變量,而不僅僅是傳遞參數。

類型變化的開放數組參數

  除了類型固定的開放數組外,Delphi 還允許定義類型變化的甚至無類型的開放數組。這種特殊類型的數組元素可隨意變化,能很方便地用作傳遞參數。

  技術上,array of const 類型的數組就能實現把不同類型、不同個數元素組成的數組一下子傳遞給例程。如下面Format 函數的定義(第七章中你將看到怎樣使用這個函數):

function Format (const Format: string;
  const Args: array of const): string;

  上面第二個參數是個開放數組,該數組元素可隨意變化。如你可以按以下方式調用這個函數:

N := 20;
S := 'Total:';
Label1.Caption := Format ('Total: %d', [N]);
Label2.Caption := Format ('Int: %d, Float: %f', [N, 12.4]);
Label3.Caption := Format ('%s %d', [S, N * 2]);

  從上可見,傳遞的參數可以是常量值、變量值或一個表達式。聲明這類函數很簡單,但是怎樣編寫函數代碼呢?怎樣知道參數類型呢?對類型可變的開放數組,其數組元素與TVarRec 類型元素兼容。

注意:不要把TVarRec 記錄類型和Variant 類型使用的TVarData 記錄類型相混淆。這兩種類型用途不同,而且互不兼容。甚至可容納的數據類型也不同,因爲TVarRec 支持Delphi 數據類型,而TVarData 支持OLE 數據類型。

TVarRec 記錄類型結構如下:

type
  TVarRec = record
    case Byte of
      vtInteger:    (VInteger: Integer; VType: Byte);
      vtBoolean:    (VBoolean: Boolean);
      vtChar:       (VChar: Char);
      vtExtended:   (VExtended: PExtended);
      vtString:     (VString: PShortString);
      vtPointer:    (VPointer: Pointer);
      vtPChar:      (VPChar: PChar);
      vtObject:     (VObject: TObject);
      vtClass:      (VClass: TClass);
      vtWideChar:   (VWideChar: WideChar);
      vtPWideChar:  (VPWideChar: PWideChar);
      vtAnsiString: (VAnsiString: Pointer);
      vtCurrency:   (VCurrency: PCurrency);
      vtVariant:    (VVariant: PVariant);
      vtInterface:  (VInterface: Pointer);
  end;

  每種記錄都有一個VType 域,乍一看不容易發現,因爲它與實際意義的整型類型數據(通常是一個引用或一個指針)放在一起,只被聲明瞭一次。

  利用上面信息我們就可以寫一個能操作不同類型數據的函數。下例的SumAll 函數,通過把字符串轉成整數、字符轉成相應的序號、True布爾值加一,計算不同類型數據的和。這段代碼以一個case語句爲基礎,雖然不得不經常通過指針取值,但相當簡單,:

function SumAll (const Args: array of const): Extended;
var
  I: Integer;
begin
  Result := 0;
  for I := Low(Args) to High (Args) do
    case Args [I].VType of
      vtInteger: Result :=
        Result + Args [I].VInteger;
      vtBoolean:
        if Args [I].VBoolean then
          Result := Result + 1;
      vtChar:
        Result := Result + Ord (Args [I].VChar);
      vtExtended:
        Result := Result + Args [I].VExtended^;
      vtString, vtAnsiString:
        Result := Result + StrToIntDef ((Args [I].VString^), 0);
      vtWideChar:
        Result := Result + Ord (Args [I].VWideChar);
      vtCurrency:
        Result := Result + Args [I].VCurrency^;
    end; // case
end;

  我已在例OpenArr中加了這段代碼,該例在按下設定的按鈕後調用SumAll 函數。

procedure TForm1.Button6Click(Sender: TObject);
var
  X: Extended;
  Y: Integer;
begin
  Y := 10;
  X := SumAll ([Y * Y, 'k', True, 10.34, '99999']);
  ShowMessage (Format (
    'SumAll ([Y*Y, ''k'', True, 10.34, ''99999'']) => %n', [X]));
end;

 

Delphi 調用協定

  32位的Delphi 中增加了新的參數傳遞方法,稱爲fastcall:只要有可能,傳遞到CPU寄存器的參數能多達三個,使函數調用操作更快。這種快速調用協定(Delphi 3確省方式)可用register 關鍵字標示。

  問題是這種快速調用協定與Windows不兼容,Win32 API 函數必須聲明使用stdcall 調用協定。這種協定是Win16 API使用的原始Pascal 調用協定和C語言使用的cdecl 調用協定的混合體。

  除非你要調用外部Windows函數或定義Windows 回調函數,否則你沒有理由不用新增的快速調用協定。 在後面你會看到使用stdcall 協定的例子,在Delphi幫助文件的Calling conventions 主題下,你能找到有關Delphi調用協定的總結內容。

什麼是方法?

  如果你使用過Delphi 或讀過Delphi 手冊,大概已經聽說過“方法”這個術語。方法是一種特殊的函數或過程,它與類這一數據類型相對應。在Delphi 中,每處理一個事件,都需要定義一個方法,該方法通常是個過程。不過一般“方法”是指與類相關的函數和過程。

  你已經在本章和前幾章中看到了幾個方法。下面是Delphi 自動添加到窗體源代碼中的一個空方法:

procedure TForm1.Button1Click(Sender: TObject);
begin
  {here goes your code}
end;

Forward 聲明

  當使用一個標識符(任何類型)時,編譯器必須已經知道該標識符指的是什麼。爲此,你通常需要在例程使用之前提供一個完整的聲明。然而在某些情況下可能做不到這一點,例如過程A調用過程B,而過程B又調用過程A,那麼你寫過程代碼時,不得不調用編譯器尚未看到其聲明的例程。

  欲聲明一個過程或函數,而且只給出它的名字和參數,不列出其實現代碼,需要在句尾加forward 關鍵字:

procedure Hello; forward;

  在後面應該補上該過程的完整代碼,不過該過程代碼的位置不影響對它的調用。下面的例子沒什麼實際意義,看過後你會對上述概念有所認識:

procedure DoubleHello; forward;

procedure Hello;
begin
  if MessageDlg ('Do you want a double message?',
      mtConfirmation, [mbYes, mbNo], 0) = mrYes then
    DoubleHello
  else
    ShowMessage ('Hello');
end;

procedure DoubleHello;
begin
  Hello;
  Hello;
end;

  上述方法可用來寫遞歸調用:即DoubleHello 調用Hello,而Hello也可能調用DoubleHello。當然,必須設置條件終止這個遞歸,避免棧的溢出。上面的代碼可以在例DoubleH 中找到,只是稍有改動。

  儘管 forward 過程聲明在Delphi中不常見,但是有一個類似的情況卻經常出現。當你在一個單元(關於單元的更多內容見下一章)的interface 部分聲明一個過程或一個函數時,它被認爲是一個forward聲明,即使沒有forward關鍵字也一樣。實際上你不可能把整個例程的代碼放在interface 部分,不過你必須在同一單元中提供所聲明例程的實現。

  類內部的方法聲明也同樣是forward聲明,當你給窗體或其組件添加事件時, Delphi會自動產生相應的代碼。在TForm 類中聲明的事件是forward 聲明,事件代碼放在單元的實現部分。下面摘錄的源代碼中有一個Button1Click 方法聲明:

type
  TForm1 = class(TForm)
    ListBox1: TListBox;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  end;

過程類型

  Object Pascal 的另一個獨特功能是可定義過程類型。過程類型屬於語言的高級功能,Delphi 程序員不會經常用到它。因爲後面章節要討論相關的內容(尤其是“方法指針” Delphi用得特別多),這裏不妨先了解一下。如果你是初學者,可以先跳過這部分,當學到一定程度後再回過頭閱讀這部分。

  Pascal 中的過程類型與C語言中的函數指針相似。過程類型的聲明只需要參數列表;如果是函數,再加個返回值。例如聲明一個過程類型,該類型帶一個通過引用傳遞的整型參數:

type
  IntProc = procedure (var Num: Integer);

  這個過程類型與任何參數完全相同的例程兼容(或用C語言行話來說,具有相同的函數特徵)。下面是一個兼容例程:

procedure DoubleTheValue (var Value: Integer);
begin
  Value := Value * 2;
end;

注意:在16位Delphi中,如果要將例程用作過程類型的實際值,必須用far指令聲明該例程。

  過程類型能用於兩種不同的目的:聲明過程類型的變量;或者把過程類型(也就是函數指針)作爲參數傳遞給另一例程。利用上面給定的類型和過程聲明,你可以寫出下面的代碼:

var
  IP: IntProc;
  X: Integer;
begin
  IP := DoubleTheValue;
  X := 5;
  IP (X);
end;

這段代碼與下列代碼等效:

var
  X: Integer;
begin
  X := 5;
  DoubleTheValue (X);
end;

  上面第一段代碼明顯要複雜一些,那麼我們爲什麼要用它呢?因爲在某些情況下,調用什麼樣的函數需要在實際中決定,此時程序類型就很有用。這裏不可能建立一個複雜的例子來說明這個問題,不過可以探究一下簡單點的例子,該例名爲ProcType。該例比前面所舉的例子都複雜,更接近實際應用。

  如圖6.3所示,新建一個工程,在上面放兩個radio按鈕和一個push按鈕。例中有兩個過程,一個過程使參數的值加倍,與前面的DoubleTheValue過程相似;另一個過程使參數的值變成三倍,因此命名爲TripleTheValue

procedure TripleTheValue (var Value: Integer);
begin
  Value := Value * 3;
  ShowMessage ('Value tripled: ' + IntToStr (Value));
end;

  兩個過程都有結果顯示,讓我們知道他們已被調用。這是一個簡單的程序調試技巧,你可以用它來檢測某一代碼段是否或何時被執行,而不用在代碼中加斷點。

  當用戶按Apply 按鈕,程序會根據radio按鈕狀態選擇執行的過程。實際上,當窗體中有兩個radio按鈕時,你只能選擇一個,因此你只需要在Apply 按鈕的OnClick 事件中添加代碼檢測radio按鈕的值,就能實現程序要求。不過爲了演示過程類型的使用,我捨近求遠選擇了麻煩但有趣的方法:只要用戶選中其中一個radio按鈕,按鈕對應的過程就會存入過程變量:

procedure TForm1.DoubleRadioButtonClick(Sender: TObject);
begin
  IP := DoubleTheValue;
end;

當用戶按Apply 按鈕,程序就執行過程變量保存的過程:

procedure TForm1.ApplyButtonClick(Sender: TObject);
begin
  IP (X);
end;

  爲了使三個不同的函數能訪問IP和 X變量,需要使變量在整個窗體單元中可見,因此不能聲明爲局部變量(在一個方法中聲明)。一個解決辦法是,把這些變量放在窗體聲明中:

type
  TForm1 = class(TForm)
    ...
  private
    { Private declarations }
    IP: IntProc;
    X: Integer;
  end;

To:chris2019(牛虻---最後的匈奴)
---------------------
--------------------- 
TForm1=Class(TForm);
//這句話指是TForm1是從TForm繼乘過來的一個子類
Button1=TButton
//這種用法好象沒有
TButton1=Class(TButton)
//這句話也是指TButton1是從TButton中繼乘過來的一個子類
var
  Form1:TForm1;
//這是指定義了一個TForm1類的對象變量。 

  學完下一章,你會更清楚地瞭解這段代碼的意思,目前只要能知道怎樣添加過程類型定義、怎樣修改相應的代碼就行了。爲了用適當的值初始化上面代碼中的兩個變量,你可以調用窗體的OnCreate 事件(激活窗體後,在Object Inspector中選擇這一事件,或者雙擊窗體)。此外最好仔細看一看上例完整的源代碼。

在第九章的 Windows 回調函數一節,你能看到使用過程類型的實例

函數重載

  重載的思想很簡單:編譯器允許你用同一名字定義多個函數或過程,只要它們所帶的參數不同。實際上,編譯器是通過檢測參數來確定需要調用的例程。

下面是從VCL的數學單元(Math Unit)中摘錄的一系列函數:

function Min (A,B: Integer): Integer; overload;
function Min (A,B: Int64): Int64; overload;
function Min (A,B: Single): Single; overload;
function Min (A,B: Double): Double; overload;
function Min (A,B: Extended): Extended; overload;

當調用方式爲Min (10, 20)時,編譯器很容易就能判定你調用的是上列第一個函數,因此返回值也是個整數。

聲明重載函數有兩條原則:

  • 每個例程聲明後面必須添加overload 關鍵字。
  • 例程間的參數個數或(和)參數類型必須不同,返回值不能用於區分各例程。

  下面是ShowMsg 過程的三個重載過程。我已把它們添加到例OverDef 中(一個說明重載和確省參數的應用程序):

procedure ShowMsg (str: string); overload;
begin
  MessageDlg (str, mtInformation, [mbOK], 0);
end;

procedure ShowMsg (FormatStr: string;
  Params: array of const); overload;
begin
  MessageDlg (Format (FormatStr, Params),
    mtInformation, [mbOK], 0);
end;

procedure ShowMsg (I: Integer; Str: string); overload;
begin
  ShowMsg (IntToStr (I) + ' ' + Str);
end;

三個過程分別用三種不同的方法格式化字符串,然後在信息框中顯示字符串。下面是三個例程的調用:

ShowMsg ('Hello');
ShowMsg ('Total = %d.', [100]);
ShowMsg (10, 'MBytes');

  令我驚喜的是Delphi的代碼參數技術與重載過程及函數結合得非常好。當你在例程名後面鍵入左圓括號時,窗口中會顯示所有可用例程的參數列表,當你輸入參數時,Delphi會根據所輸入參數的類型過濾參數列表。從圖6.4你可看到,當開始輸入一個常量字符串時,Delphi只顯示第一個參數爲字符串的兩個ShowMsg例程參數列表,濾掉了第一個參數爲整數的例程。

  重載例程必須用overload關鍵字明確標示,你不能在同一單元中重載沒有overload標示的例程,否則會出現錯誤信息: "Previous declaration of '<name>' was not marked with the 'overload' directive."。不過你可以重載在其他單元中聲明的例程,這是爲了與以前的Delphi版本兼容,以前的Delphi版本允許不同的單元重用相同的例程名。無論如何,這是例程重載的特殊情況不是其特殊功能,而且不小心會出現問題。

例如在一個單元中添加以下代碼:

procedure MessageDlg (str: string); overload;
begin
  Dialogs.MessageDlg (str, mtInformation, [mbOK], 0);
end;

這段代碼並沒有真正重載原始的MessageDlg 例程,實際上如果鍵入:

MessageDlg ('Hello');

你將得到一個有意思的錯誤消息,告訴你缺少參數。調用本地例程而不是VCL的唯一途徑是明確標示例程所在單元,這有悖於例程重載的思想:

OverDefF.MessageDlg ('Hello');

確省參數

Delphi 4 中添加了一個新功能,即允許你給函數的參數設定確省值,這樣調用函數時該參數可以加上,也可以省略。下例把應用程序全程對象的MessageBox 方法重新包裝了一下,用PChar 替代字符串,並設定兩個確省值:

procedure MessBox (Msg: string;
  Caption: string = 'Warning';
  Flags: LongInt = mb_OK or mb_IconHand);
begin
  Application.MessageBox (PChar (Msg),
    PChar (Caption), Flags);
end;

使用這一定義,你就可以用下面任一種方式調用過程:

MessBox ('Something wrong here!');
MessBox ('Something wrong here!', 'Attention');
MessBox ('Hello', 'Message', mb_OK);

從圖6.5中可以看到,Delphi的 代碼參數提示條會用不同的風格顯示確省值參數,這樣你就很容易確定哪個參數是可以省略的。

注意一點,Delphi 不產生任何支持確省參數的特殊代碼,也不創建例程的多份拷貝,缺省參數是由編譯器在編譯時添加到調用例程的代碼中。

  使用確省參數有一重要限定:你不能“跳過”參數,如省略第二個參數後,不能把第三個參數傳給函數:

MessBox ('Hello', mb_OK); // error  

  確省參數使用主要規則:調用時你只能從最後一個參數開始進行省略,換句話說,如果你要省略一個參數,你必須省略它後面所有的參數。

  確省參數的使用規則還包括:

  • 帶確省值的參數必須放在參數表的最後面。
  • 確省值必須是常量。顯然,這限制了確省參數的數據類型,例如動態數組和界面類型的確省參數值只能是 nil;至於記錄類型,則根本不能用作確省參數。
  • 確省參數必須通過值參或常參傳遞。引用參數 var不能有缺省值。

  如果同時使用確省參數和重載可能會出現問題,因爲這兩種功能可能發生衝突。例如把以前ShowMsg 過程改成:

procedure ShowMsg (Str: string; I: Integer = 0); overload;
begin
  MessageDlg (Str + ': ' + IntToStr (I),
    mtInformation, [mbOK], 0);
end;

  編譯時編譯器不會提出警告,因爲這是合法的定義。

  然而編譯調用語句:

ShowMsg ('Hello');

  編譯器會顯示 Ambiguous overloaded call to 'ShowMsg'.( 不明確重載調用ShowMsg)。注意,這條錯誤信息指向新定義的重載例程代碼行之前。實際上,用一個字符串參數無法調用ShowMsg 過程,因爲編譯器搞不清楚你是要調用只帶字符串參數的ShowMsg 過程,還是帶字符串及整型確省參數的過程。遇到這種問題時,編譯器不得不停下來,要求你明確自己的意圖。

結束語

  過程和函數是編程的一大關鍵,Delphi 中的方法就是與類及對象關聯的過程和函數。

下面幾章將從字符串開始詳細講解Pascal的一些編程元素。

下一頁: 字符串操作

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章