参数 (程序设计)
在程序设计中,参数(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.50
。return
表示函数将产生的结果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
具有两个参数,分别名为addend1
和addend2
,这个函数将传递给参数的值相加,然后将结果返回给子例程的调用方(使用C语言编译器自动提供的技术)。
调用Sum
函数的代码如下所示:
int value1 = 40;
int value2 = 2;
int sum_value = Sum(value1, value2);
变量value1
和value2
用值初始化。在这种情况下,value1
和value2
都是sum
函数的引数。
在运行时,分配给这些变量的值作为参数传递给函数Sum
。在Sum
函数中,将会对参数addend1
和addend2
进行求值,分别得出参数40和2。将添加参数的值,并将结果返回给调用方,在此将其分配给变量sum_value
。
由于参数和引数之间的差异,有可能向过程提供不合适的引数,也可能提供过多或过少的引数,要么一个或多个引数可能是错误的类型,或者是以错误的顺序提供了引数。这些情况中的任何一种都会导致参数表表和引数列表之间不匹配,并且该过程通常会返回意外的答案或生成执行期错误。
数据类型
在强类型编程语言中,必须在过程声明中指定每个参数的类型。使用类型推断的语言会尝试从函数的主体和用法中自动推断类型。动态类型的编程语言则会将类型解析推迟到运行时。弱类型语言几乎没有类型解析,甚至依靠程序员来确保正确性。
一些语言使用特殊的关键字(例如void
)来指示该子例程没有参数。在形式类型理论中,此类函数采用空参数表表(其类型不是void
,而是unit
)。
引数传递
将参数分配给参数的确切机制称为参数传递(argument passing),该机制取决于可以使用关键字来指定的参数求值策略(通常为按值传递)。
默认引数
某些编程语言(例如Ada,C++,Clojure,Common Lisp,Fortran 90,Python,Ruby,Tcl和Windows PowerShell)允许在子例程的声明中显式或隐式给出默认引数。这允许调用者在调用子例程时忽略该参数。如果显式给出了默认引数,则如果调用者未提供该值,则使用该值。如果默认引数是隐式的(有时通过使用诸如Optional
之类的关键字),则该语言将提供一个通常值(例如null
、Empty
、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)许子例程具有命名参数。这使调用代码更具自记录性。它还为调用者提供了更大的灵活性,通常允许更改参数的顺序,或者根据需要省略参数。
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演算中的函数应用是左关系的,因此,这种操作看起来会像是将函数应用于多个引数的函数先将函数应用于第一个引数,然后将结果函数应用于第二个自变量,并依此类推。
输出参数
输出参数,也称为出参或返回参数,是一种用于输出而不是更通常的使用用于输入的参数。在某些语言中,尤其是C和C ++,输出参数是一种习惯用法,而其他语言则内置了对输出参数的支持。内置支持输出参数的语言包括Ada[6]、Fortran、SQL的各种拓展程序(例如:PL/SQL[7]和Transact-SQL)、C#[8]和.Net Framework[9]、Swift[10]以及脚本语言TScript。
更准确地说,参数模式或参数共有三种类型:输入参数(入参)、输出参数(出参)、输入输出参数(出入参),这些参数往往使用in
、out
、inout
来进行表示。输入引数输入引数(针对输入参数的引数)必须是一个值,例如已经被初始化了的变量或常值,而且不能重新定义获分配它。输出引数必须是可分配变量,但不必初始化而且任何现有值均不可访问输入值,执行函数后必须分配一个值。输入输出引数必须是已初始化了的可分配变量,并且可以选择给其分配一个值。确切的要求和执行情况因语言而异,例如在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#中,按值传递输入参数(默认,无关键字),而按引用传递输出和输入/输出参数(out
和ref
),而在PL / SQL中传递输入参数(IN
)通过引用,默认情况下通过值传递输出和输入/输出参数(OUT
和IN OUT
),并将结果复制回去,但是可以使用NOCOPY
编译器提示通过引用传递。[7]
与输出参数在语法上相似的构造是将返回值分配给与函数名称相同的变量。 可以在Pascal和Fortran 66和Fortran 77中发现,此处使用Pascal示例:
function f(x, y: integer): integer;
begin
f := x + y;
end;
这在语义上是不同的,因为在调用时仅对函数进行求值–不会从调用范围传递变量来存储输出。
使用
输出参数的主要用途是从一个函数返回多个值,而输入/输出参数的用途是使用参数传递(而不是像全局变量那样在共享环境中)来修改状态。 返回多个值的一个重要用途是解决返回值和错误状态的半谓词问题。
例如,要从C函数中返回两个变量,可以写:
int width
int height;
F(x, &width, &height);
其中x
是输入参数, width
和height
是输出参数。
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()
为了返回几种类型之一的值,可以使用标签联合 。最常见的情况是可空类型 ( 可选类型 ),其中返回值可以为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]
相关条目
- 求值策略
- 运算符重载
- 自由变量和约束变量
参考资料
- ^ Prata, Stephen. C primer plus 5th. Sams. 2004: 276–277. ISBN 978-0-672-32696-7.
- ^ Working Draft, Standard for Programming Language C++ (PDF). www.open-std.org. [1 January 2018]. (原始内容 (PDF)存档于2005-12-14).
- ^ Gordon, Aaron. Subprograms and Parameter Passing. rowdysites.msudenver.edu/~gordona. [1 January 2018]. (原始内容存档于2018-01-01).
- ^ KathleenDollard. Passing Arguments by Value and by Reference - Visual Basic. docs.microsoft.com. [2020-04-21]. (原始内容存档于2019-09-06) (美国英语).
- ^ The GNU C Programming Tutorial. crasseux.com. [2020-04-21]. (原始内容存档于2020-02-16).
- ^ 6.0 6.1 Ada '83 Rationale, Sec 8.2: Parameter Modes. archive.adaic.com. [2020-04-21]. (原始内容存档于2013-10-06).
- ^ 7.0 7.1 PL/SQL Subprograms. docs.oracle.com. [2020-04-21]. (原始内容存档于2020-06-14).
- ^ 8.0 8.1 Msdn forums - Visual C# Language. social.msdn.microsoft.com. [2020-04-21]. (原始内容存档于2019-09-04).
- ^ stevestein. ParameterDirection Enum (System.Data). docs.microsoft.com. [2020-04-21] (美国英语).
- ^ Functions — The Swift Programming Language (Swift 5.2). docs.swift.org. [2020-04-21]. (原始内容存档于2020-03-29).
- ^ dotnet-bot. Int32.TryParse Method (System). docs.microsoft.com. [2020-04-21] (美国英语).
- ^ 12.0 12.1 jillre. CA1021: Avoid out parameters - Visual Studio 2015. docs.microsoft.com. [2020-04-21] (美国英语).