參數 (程式設計)

程式中用於向子程式傳遞資料的特殊變數
(重定向自参数 (程式设计)

在程式設計中,參數parameter)又稱形式引數formal argument),是一種在调用子程序时用以向子程序传递資料的特殊变量,這些被傳遞資料也就是子程序引數arguments)的值。[1][2][3]參數的有序列表通常包含在子例程的定義中,因此在每次调用子例程時也會計算這些傳入的引數,並且將對應資料送到子程序中。

不同於在數學中通常所使用的引數,計算機科學中的引數是在引動過程(invocation)或者呼叫陳述式(call statement)中傳遞給函式(function)、程序(procedure)或者常式(routine)的實際輸入表達式,而參數是子程序實現內部的變量。例如,定義一個add子程序為def add(x, y): return x + y那麼x,y 就是一對參數。當呼叫這個子程序時,例如add(2, 3)那麼,2,3就是一對引數,請注意,呼叫上下文中的變量(及其表達式)可以是引數,如果以子程序的形式呼叫a = 2; b = 3; add(a, b)則變量a, b是引數,而不是值2, 3

在最常見的傳值調用情況下,參數會在子程序中充當新的局部變量,並初始化為引數的值(如果引數是變量,則為引數的局部(隔離)副本)。但在例如傳引用調用的其他情況下,調用者提供的引數變量可能會受到被調用子例程中操作的影響。

所使用的語言一般定義了如何聲明參數以及將引數(的值)傳遞給子例程的參數的語義,但是在任何特定計算機系統中如何表示該參數的細節取決於該系統的調用約定

示例

以下使用C語言編寫的程序定義了一個名為SalesTax的函數,並具有一個名為price的參數。price的類型是double,也就是雙精度浮點數。該函數的返回類型也是double

double SalesTax( double price ){
  return 0.05 * price;
}

定義函數後,可以按以下方式調用它:

SalesTax(10.00);

示例中使用了引數10.00調用該函數,此時值10.00就會分配給price,然後該函數開始計算其結果。產生結果的步驟在下面指定,並用{}括起來。0.05 * price表示要做的第一件事是將價格乘以0.05,即得出0.50return表示函數將產生的結果0.05 * price返回。因此,最終結果(忽略將小數部分表示為二進制時可能遇到的捨入誤差)為0.50

參數和引數

術語參數引數在不同的編程語言中可能會具有不同的含義。有時它們可以互換使用,並且以上下文區分含義。術語參數(也稱為形式參數(形參,formal parameter))通常用來指該變量作為在函數定義中變量的內容。而引數(也稱為實際參數(實參,actual parameter))是指在函數調用中提供的實際輸入。例如,如果將一個函數定義為def f(x): ...x則為參數。若定義有a = ...; f(a)來呼叫函數,那麼這裡的a就是引數。

參數是一個(未綁定實際值的)變量,而引數可以是一個值或變量,或者是涉及值和變量的更複雜的表達式。在由值呼叫的情況下,所傳遞給函數是自變量的值。例如f(2)a = 2; f(a)對函數的呼叫是相同的,在以變量作為參數通過引用調用時,若函數調用的語法可以保持不變,則傳遞的是對該變量的引用。[4]具體[[求值策略#传值调用(Call by value)|按值傳遞]]或者是[[求值策略#传引用调用(Call by reference)|按引用傳遞]]的細節規範則由函數的聲明以及定義中進行。

一般而言,參數出現在過程定義中,引數則出現在過程調用中。在函數f(x) = x*x的定義中,變量x是參數。在函數f(2)調用中,值2是該函數的引數。簡單而不太準確的概擴來說參數是類型,而引數是實作。

參數是過程的固有屬性,包含在其定義中。例如,在許多語言中,將兩個提供的整數加在一起併計算總和的過程將需要兩個參數,每個整數一個。通常,可以使用任意數量的參數或完全不使用參數來定義過程。如果過程具有參數,則其定義中指定參數的部分稱為其參數列表(parameter list)。

相反,引數是調用過程時提供給過程的表達式,[5]其通常是一個與參數之一匹配的表達式。與參數(它們構成過程定義的不變部分)不同,自變量在調用之間可能有所不同。每次調用過程時,過程調用中指定引數的部分稱為引數列表(argument list)。

儘管參數通常也被混稱為引數,但是在運行時調用子例程時,有時會將引數視為分配給參數變量的實際值或引用。在討論正在調用子例程的程式碼時,傳遞給子例程的任何值或引用都是引數。

在討論子例程定義中的程式碼時,子例程的參數列表中的變量是參數,而運行時參數的值則會是引數。例如,在C語言中,處理線程時通常會傳入void *類型的參數並將其強制轉換為預期的類型:

void ThreadFunction( void* pThreadArgument )
{
  //將第一個參數命名為pThreadArgument而不是pThreadParameter是正確的。
  //在運行時,我們使用的值是一個引數。
  //如上所述,在討論子例程定義時保留術語參數。
}

為了更好地理解它們之間的區別,請考慮以下用C語言編寫的函數:

int Sum( int addend1, int addend2 )
{
  return addend1 + addend2;
}

函數Sum具有兩個參數,分別名為addend1addend2,這個函數將傳遞給參數的值相加,然後將結果返回給子例程的調用方(使用C語言編譯器自動提供的技術)。 調用Sum函數的代碼如下所示:

int value1 = 40;
int value2 = 2;
int sum_value = Sum(value1, value2);

變量value1value2用值初始化。在這種情況下,value1value2都是sum函數的引數。

在運行時,分配給這些變量的值作為參數傳遞給函數Sum。在Sum函數中,將會對參數addend1addend2進行求值,分別得出參數40和2。將添加參數的值,並將結果返回給調用方,在此將其分配給變量sum_value

由於參數和引數之間的差異,有可能向過程提供不合適的引數,也可能提供過多或過少的引數,要麼一個或多個引數可能是錯誤的類型,或者是以錯誤的順序提供了引數。這些情況中的任何一種都會導致參數列表和引數列表之間不匹配,並且該過程通常會返回意外的答案或生成執行期錯誤。

數據類型

強類型編程語言中,必須在過程聲明中指定每個參數的類型。使用類型推斷的語言會嘗試從函數的主體和用法中自動推斷類型。動態類型的編程語言則會將類型解析推遲到運行時。弱類型語言幾乎沒有類型解析,甚至依靠程序員來確保正確性。

一些語言使用特殊的關鍵字(例如void)來指示該子例程沒有參數。在形式類型理論中,此類函數採用空參數列表(其類型不是void,而是unit英语Unit type)。

引數傳遞

將參數分配給參數的確切機制稱為參數傳遞(argument passing),該機制取決於可以使用關鍵字來指定的參數求值策略(通常為按值傳遞)。

預設參數

某些編程語言(例如AdaC++ClojureCommon LispFortran 90PythonRubyTclWindows PowerShell)允許在子例程的聲明中顯式或隱式給出缺省参数。這允許調用者在調用子例程時忽略該參數。如果顯式給出了預設參數,則如果調用者未提供該值,則使用該值。如果預設參數是隱式的(有時通過使用諸如Optional之類的關鍵字),則該語言將提供一個通常值(例如nullEmpty、0、空字串等)。

PowerShell示例:

 function doc($g = 1.21) {
   "$g gigawatts? $g gigawatts? Great Scott!"
 }
PS  > doc
1.21 gigawatts? 1.21 gigawatts? Great Scott!

PS  > doc 88
88 gigawatts? 88 gigawatts? Great Scott!

預設參數可以看作是可變長度引數列表的一種特殊情況。

可變長度參數列表

某些語言允許將子例程定義為接受可變數量的引數。對於此類語言,子例程必須遍歷參數列表。

PowerShell示例:

 function marty {
   $args | foreach { "back to the year $_" }
 }
PS  > marty 1985
back to the year 1985

PS  > marty 2015 1985 1955
back to the year 2015
back to the year 1985
back to the year 1955

命名參數

某些編程語言(例如Ada和Windows PowerShell)許子例程具有命名參數。這使調用代碼更具自記錄性英语Self-documenting_code。它還為調用者提供了更大的靈活性,通常允許更改參數的順序,或者根據需要省略參數。

PowerShell示例:

 function jennifer($adjectiveYoung, $adjectiveOld) {
   "Young Jennifer: I'm $adjectiveYoung!"
   "Old Jennifer: I'm $adjectiveOld!"
 }
PS  > jennifer 'fresh' 'experienced'
Young Jennifer: I'm fresh!
Old Jennifer: I'm experienced!

PS  > jennifer -adjectiveOld 'experienced' -adjectiveYoung 'fresh'
Young Jennifer: I'm fresh!
Old Jennifer: I'm experienced!

函式語言中的多個參數

lambda演算中,每個函數只有一個參數。通常認為具有多個參數的函數在lambda演算中表示為具有第一個引數的函數,並返回具有其餘引數的函數。這種轉換也被稱為柯里化(Currying)。一些編程語言(例如ML和Haskell)遵循此方案。在這些語言中,每個函數都只有一個參數,看起來像多個參數的函數的定義,實際上是返回一個函數的函數的定義的語法糖等。在這些語言以及lambda演算中的函數應用是左關聯英语Operator associativity的,因此,這種操作看起來會像是將函數應用於多個引數的函數先將函數應用於第一個引數,然後將結果函數應用於第二個自變量,並依此類推。

輸出參數

輸出參數,也稱為出參返回參數,是一種用於輸出而不是更通常的使用用於輸入的參數。在某些語言中,尤其是C和C ++,輸出參數是一種習慣用法,而其他語言則內置了對輸出參數的支持。內置支持輸出參數的語言包括Ada[6]FortranSQL的各種拓展程式(例如:PL/SQL[7]Transact-SQL)、C#[8].Net Framework[9]Swift[10]以及手稿語言TScript

更準確地說,參數模式參數共有三種類型:輸入參數(入參)、輸出參數(出參)、輸入輸出參數(出入參),這些參數往往使用inoutinout來進行表示。輸入引數輸入引數(針對輸入參數的引數)必須是一個值,例如已經被初始化了的變數或常值,而且不能重新定義獲分配它。輸出引數必須是可分配變數,但不必初始化而且任何現有值均不可存取輸入值,執行函數後必須分配一個值。輸入輸出引數必須是已初始化了的可分配變數,並且可以選擇給其分配一個值。確切的要求和執行情況因語言而異,例如在Ada 83中,即使分配後也只能分配輸出參數(在Ada 95中刪除了該參數,以消除對輔助累加器變量的需要)。這有些類似於的概念,也就是左值(l-value,具有值)、右值(r-value,可分配)以及左右值(l-value/r-value,具有值且可分配),不過這些術語在C語言中有特殊含意。

在某些情況下,只有輸入和輸入輸出是有區別的,而輸出則被視為輸入/輸出的特定用途,而在其他情況下,僅支持輸入和輸出(但不支持輸入輸出)。默認模式因語言而異:在Fortran 90中,默認輸入/輸出,在C#和SQL擴展中,默認輸入/輸出,在TScript中,每個參數都明確指定為輸入或輸出。

從語法上講,參數模式通常在函數聲明(例如在C#中:void f(out int x))中用關鍵字表示。通常,輸出參數通常放在參數列表的末尾以清楚地區分它們。TScript使用另一種方​​法,其中在函數聲明中列出了輸入參數,然後列出了輸出參數,並用冒號(:)分隔,並且函數本身沒有返回類型,如在此函數中一樣,該函數用於計算文本的大小分段:

 TextExtent(WString text, Font font : Integer width, Integer height)

參數模式是指稱語義的一種形式,其表明了程式設計師的意圖並允許編譯器捕獲錯誤來應用優化,它們不​​一定暗示操作語義(參數傳遞的實際發生方式)。值得注意的是,雖然輸入參數可以通過值調用實現,而輸出和輸入輸出參數可以通過引用調用實現,這是在沒有內置支持的情況下以語言實現這些模式的直接方法,但也並非總是如此實施。《Ada '83基本原理(英語:Ada '83 Rationale)》中詳細討論了這種區別,其中強調了參數模式是從中實際實現的參數傳遞機制(通過引用或通過複製)抽像出來的。[6]例如,在C#中,按值傳遞輸入參數(默認,無關鍵字),而按引用傳遞輸出和輸入/輸出參數(outref),而在PL / SQL中傳遞輸入參數(IN)通過引用,默認情況下通過值傳遞輸出和輸入/輸出參數(OUTIN OUT),並將結果復制回去,但是可以使用NOCOPY編譯器提示通過引用傳遞。[7] 與輸出參數在語法上相似的構造是將返回值分配給與函數名稱相同的變量。 可以在Pascal和Fortran 66和Fortran 77中發現,此處使用Pascal示例:

function f(x, y: integer): integer;
begin
    f := x + y;
end;

這在語義上是不同的,因為在調用時僅對函數進行求值–不會從調用範圍傳遞變數來存儲輸出。

使用

輸出參數的主要用途是從一個函數返回多個值,而輸入/輸出參數的用途是使用參數傳遞(而不是像全局變量那樣在共享環境中)來修改狀態。 返回多個值的一個重要用途是解決返回值和錯誤狀態的半謂詞問題英语Semipredicate problem

例如,要從C函數中返回兩個變數,可以寫:

int width
int height;

F(x, &width, &height);

其中x是輸入參數, widthheight是輸出參數。 C和相關語言的一個常見用例是異常處理 ,其中函數將返回值放在輸出變量中,並返回一個布爾值,該布爾值對應於函數是否成功。 一個典型的例子是.NET中的TryParse方法,尤其是C#,它將字符串解析為整數,成功則返回true ,失敗則返回false 。 其具有以下形式:[11]

public static bool TryParse(string s, out int result)

可以這樣使用:

int result;
if (!Int32.TryParse(s, result)) {
    // exception handling
}

類似的考慮適用於返回幾種可能類型之一的值,其中返回值可以指定類型,然後將值存儲在幾種輸出變量之一中。

缺點

在現代編程中,通常不鼓勵使用輸出參數,因為它們笨拙、令人困惑且級別太低,普通的返回值相當容易理解和使用。[12]值得注意的是,輸出參數涉及具有副作用(修改輸出參數)的函數,並且在語義上與引用相似,比純函數和值更容易混淆,並且輸出參數與輸入/輸出參數之間的區別可能微妙。 此外,由於在通常的編程樣式中,大多數參數只是輸入參數,因此輸出參數和輸入輸出參數是異常的,從而容易引起誤解。

輸出和輸入輸出參數會阻止函數組合,因為輸出存儲在變量中,而不是表達式的值中。 因此,必須首先聲明一個變量,然後函數鏈的每一步都必須是一個單獨的語句。 例如,在C ++中,以下函數組成:

Object obj = G(y, F(x));

當寫有輸出和輸入輸出參數時,可以寫為(對於F是輸出參數,對於G是輸入/輸出參數):

Object obj;
F(x, &obj);
G(y, &obj);

在具有單個輸出或輸入/輸出參數且沒有返回值的函數的特殊情況下,如果該函數還返回輸出或輸入輸出參數(或在C / C ++中是其地址),則可以構成函數,在這種情況下,以上內容將變為:

Object obj;
G(y, F(x, &obj));

替代方式

輸出參數的用例有多種選擇。

為了從一個函數返回多個值,一種替代方法是返回一個元組 。 從語法上講,如果可以使用自動序列拆包和並行賦值 (例如在Go或Python中),這會更清楚:

def f():
    return 1, 2
a, b = f()

為了返回幾種類型之一的值,可以使用標籤聯合 。最常見的情況是可空類型英语Nullable type可選型別 ),其中返回值可以為null以指示失敗。對於異常處理,可以返回可為null的類型,也可以引發異常。 例如,在Python中,可能有以下兩種情況之一:

result = Parse(s)
if result is None:
    # exception handling

或者,更慣用的方法:

try:
    result = Parse(s)
except ParseError:
    # exception handling

不需要局部變量並在使用輸出變量時復制返回值的微優化也可以通過足夠複雜的編譯器應用於常規函數和返回值。[8] 使用C和相關語言輸出參數的通常替代方法是返回包含所有返回值的單個數據結構。 例如,給定一個封裝寬度和高度的結構,可以這樣寫:

WidthHeight width_and_height = F(x);

在面向對象的語言中,通常不使用輸入輸出參數,而是通過[[求值策略#传共享对象调用(Call by sharing)|傳共享調用]] ,將引用傳遞給對象然後對對象進行突變來使用調用 ,而不更改變量所引用的對象。[12]

相關條目

參考資料

  1. ^ Prata, Stephen. C primer plus 5th. Sams. 2004: 276–277. ISBN 978-0-672-32696-7. 
  2. ^ Working Draft, Standard for Programming Language C++ (PDF). www.open-std.org. [1 January 2018]. (原始内容 (PDF)存档于2005-12-14). 
  3. ^ Gordon, Aaron. Subprograms and Parameter Passing. rowdysites.msudenver.edu/~gordona. [1 January 2018]. (原始内容存档于2018-01-01). 
  4. ^ KathleenDollard. Passing Arguments by Value and by Reference - Visual Basic. docs.microsoft.com. [2020-04-21]. (原始内容存档于2019-09-06) (美国英语). 
  5. ^ The GNU C Programming Tutorial. crasseux.com. [2020-04-21]. (原始内容存档于2020-02-16). 
  6. ^ 6.0 6.1 Ada '83 Rationale, Sec 8.2: Parameter Modes. archive.adaic.com. [2020-04-21]. (原始内容存档于2013-10-06). 
  7. ^ 7.0 7.1 PL/SQL Subprograms. docs.oracle.com. [2020-04-21]. (原始内容存档于2020-06-14). 
  8. ^ 8.0 8.1 Msdn forums - Visual C# Language. social.msdn.microsoft.com. [2020-04-21]. (原始内容存档于2019-09-04). 
  9. ^ stevestein. ParameterDirection Enum (System.Data). docs.microsoft.com. [2020-04-21] (美国英语). 
  10. ^ Functions — The Swift Programming Language (Swift 5.2). docs.swift.org. [2020-04-21]. (原始内容存档于2020-03-29). 
  11. ^ dotnet-bot. Int32.TryParse Method (System). docs.microsoft.com. [2020-04-21] (美国英语). 
  12. ^ 12.0 12.1 jillre. CA1021: Avoid out parameters - Visual Studio 2015. docs.microsoft.com. [2020-04-21] (美国英语).