함수
함수 (Functions)
Dart에서 함수를 선언하고 다루는 모든 방법을 살펴볼게요. 파라미터 종류부터 익명 함수, 클로저, 제너레이터까지 차례로 봐요.
출처: Dart 공식 문서
본문
Dart는 진정한 객체 지향 언어라서, 함수조차 객체이고 Function이라는 타입을 가져요.
즉 함수를 변수에 할당하거나 다른 함수의 인자로 전달할 수 있어요. Dart 클래스의 인스턴스를 함수처럼 호출할 수도 있는데, 자세한 내용은 Callable objects를 참고해요.
함수를 구현하는 예시를 볼게요.
bool isNoble(int atomicNumber) {
return _nobleGases[atomicNumber] != null;
}
Effective Dart는 공개 API에 타입 어노테이션을 권장하지만, 타입을 생략해도 함수는 여전히 동작해요.
isNoble(atomicNumber) {
return _nobleGases[atomicNumber] != null;
}
표현식 하나만 포함하는 함수라면 축약 문법을 쓸 수 있어요.
bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;
=> *expr* 문법은 { return *expr*; }의 축약형이에요. => 표기는 arrow(화살표) 문법이라고 부르기도 해요.
파라미터 (Parameters)
함수는 required positional(필수 위치) 파라미터를 얼마든지 가질 수 있어요. 그 뒤에는 named(이름 있는) 파라미터가 오거나 optional positional(선택 위치) 파라미터가 올 수 있는데, 둘 다 오는 건 안 돼요.
함수에 인자를 전달하거나 함수 파라미터를 정의할 때 trailing commas를 쓸 수 있어요.
이름 있는 파라미터 (Named parameters)
이름 있는 파라미터는 명시적으로 required로 표시하지 않는 한 선택적(optional)이에요.
함수를 정의할 때는 {*param1*, *param2*, …}로 이름 있는 파라미터를 지정해요. 기본값을 제공하지 않거나 이름 있는 파라미터를 required로 표시하지 않으면, 그 타입은 nullable이어야 해요. 기본값이 null이 되기 때문이에요.
/// Sets the [bold] and [hidden] flags ...
void enableFlags({bool? bold, bool? hidden}) {
...
}
함수를 호출할 때는 *paramName*: *value*로 이름 있는 인자를 지정할 수 있어요. 예를 들면 다음과 같아요.
enableFlags(bold: true, hidden: false);
이름 있는 파라미터의 기본값을 null이 아닌 값으로 정의하려면 =로 기본값을 지정해요. 지정하는 값은 컴파일 타임 상수여야 해요. 예를 들면 다음과 같아요.
/// Sets the [bold] and [hidden] flags ...
void enableFlags({bool bold = false, bool hidden = false}) {
...
}
// bold will be true; hidden will be false.
enableFlags(bold: true);
대신 이름 있는 파라미터를 필수로 만들어 호출자가 반드시 값을 제공하도록 하려면 required로 표시하면 돼요.
const Scrollbar({super.key, required Widget child});
누군가 child 인자를 지정하지 않고 Scrollbar를 만들려고 하면, analyzer가 문제를 보고해요.
위치 인자를 먼저 두고 싶을 수도 있지만 Dart는 이를 요구하지 않아요. Dart는 API에 맞다면 인자 목록 어디에든 이름 있는 인자를 놓을 수 있게 허용해요.
repeat(times: 2, () {
...
});
선택 위치 파라미터 (Optional positional parameters)
함수 파라미터의 일부를 []로 감싸면 선택 위치 파라미터로 표시돼요. 기본값을 제공하지 않으면 그 타입은 nullable이어야 해요. 기본값이 null이 되기 때문이에요.
String say(String from, String msg, [String? device]) {
var result = '$from says $msg';
if (device != null) {
result = '$result with a $device';
}
return result;
}
선택 파라미터 없이 이 함수를 호출하는 예시를 볼게요.
assert(say('Bob', 'Howdy') == 'Bob says Howdy');
세 번째 파라미터를 넣어 호출하는 예시도 볼게요.
assert(
say('Bob', 'Howdy', 'smoke signal') ==
'Bob says Howdy with a smoke signal',
);
선택 위치 파라미터의 기본값을 null이 아닌 값으로 정의하려면 =로 기본값을 지정해요. 지정하는 값은 컴파일 타임 상수여야 해요. 예를 들면 다음과 같아요.
String say(String from, String msg, [String device = 'carrier pigeon']) {
var result = '$from says $msg with a $device';
return result;
}
assert(say('Bob', 'Howdy') == 'Bob says Howdy with a carrier pigeon');
파라미터 수정자 (Parameter modifiers)
final과 var 파라미터 수정자는 기본 생성자(primary constructors)에서 인스턴스 필드를 선언하기 위해 예약되어 있어요. 다른 어떤 선언의 형식 파라미터에 이들을 쓰면 컴파일 타임 오류(extraneous_modifier)가 나요. 코드 예시와 마이그레이션 단계는 Parameter modifier restrictions를 참고해요.
스타일 선택으로 재할당 불가 파라미터를 강제하려면, parameter_assignments lint 규칙을 대신 써요.
main() 함수
모든 앱에는 앱의 진입점(entrypoint)이 되는 최상위 main() 함수가 있어야 해요. main() 함수는 void를 반환하고, 인자를 위한 선택적 List<String> 파라미터를 가져요.
간단한 main() 함수를 볼게요.
void main() {
print('Hello, World!');
}
인자를 받는 커맨드라인 앱의 main() 함수 예시를 볼게요.
// Run the app like this: dart run args.dart 1 test
void main(List<String> arguments) {
print(arguments);
assert(arguments.length == 2);
assert(int.parse(arguments[0]) == 1);
assert(arguments[1] == 'test');
}
커맨드라인 인자를 정의하고 파싱하려면 args library를 쓸 수 있어요.
일급 객체로서의 함수 (Functions as first-class objects)
함수를 다른 함수의 파라미터로 전달할 수 있어요. 예를 들면 다음과 같아요.
void printElement(int element) {
print(element);
}
var list = [1, 2, 3];
// Pass printElement as a parameter.
list.forEach(printElement);
함수를 변수에 할당할 수도 있어요.
var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!';
assert(loudify('hello') == '!!! HELLO !!!');
이 예시는 익명 함수(anonymous function)를 사용해요. 그에 대한 자세한 내용은 다음 섹션에서 다뤄요.
함수 타입 (Function types)
함수의 타입을 지정할 수 있는데, 이를 function type(함수 타입)이라고 해요. 함수 타입은 함수 선언 헤더에서 함수 이름을 Function이라는 키워드로 바꿔 얻어요. 또한 위치 파라미터의 이름은 생략할 수 있지만, 이름 있는 파라미터의 이름은 생략할 수 없어요. 예를 들면 다음과 같아요.
void greet(String name, {String greeting = 'Hello'}) =>
print('$greeting $name!');
// Store `greet` in a variable and call it.
void Function(String, {String greeting}) g = greet;
g('Dash', greeting: 'Howdy');
익명 함수 (Anonymous functions)
대부분의 함수는 main()이나 printElement()처럼 이름을 갖지만, 이름 없는 함수도 만들 수 있어요. 이 함수들을 익명 함수(anonymous functions), 람다(lambda), 또는 *클로저(closure)*라고 불러요.
익명 함수는 이름 있는 함수처럼 다음과 같은 특징이 있어요.
- 0개 이상의, 쉼표로 구분된 파라미터
- 괄호 사이의 선택적 타입 어노테이션
아래 코드 블록이 함수의 본문이에요.
([[Type] param1[, ...]]) {
codeBlock;
}
다음 예시는 타입이 없는 파라미터 item을 가진 익명 함수를 map 함수에 전달해요. 리스트의 각 항목에 대해 호출되는 map 함수는 각 문자열을 대문자로 변환해요. 그런 다음 for-in 루프가 변환된 각 문자열을 그 길이와 함께 출력해요.
const list = ['apples', 'bananas', 'oranges'];
var uppercaseList = list.map((item) {
return item.toUpperCase();
}).toList();
// Convert to list after mapping
for (var item in uppercaseList) {
print('$item: ${item.length}');
}
Run을 눌러 코드를 실행해 보세요.
void main() {
const list = ['apples', 'bananas', 'oranges'];
var uppercaseList = list.map((item) {
return item.toUpperCase();
}).toList();
// Convert to list after mapping
for (var item in uppercaseList) {
print('$item: ${item.length}');
}
}
함수가 단일 표현식이나 return 문 하나만 포함한다면, 화살표 표기로 줄일 수 있어요. 다음 줄을 DartPad에 붙여 넣고 Run을 눌러 기능적으로 동일한지 확인해 보세요.
var uppercaseList = list.map((item) => item.toUpperCase()).toList();
어휘적 유효 범위 (Lexical scope)
Dart는 코드의 배치에 따라 변수의 유효 범위를 결정해요. 이런 특성을 가진 프로그래밍 언어를 어휘적 유효 범위(lexically scoped) 언어라고 해요. 변수가 유효 범위에 있는지 확인하려면 "중괄호를 바깥으로 따라가면" 돼요.
예시: 각 유효 범위 레벨마다 변수를 가진 일련의 중첩 함수를 볼게요.
bool topLevel = true;
void main() {
var insideMain = true;
void myFunction() {
var insideFunction = true;
void nestedFunction() {
var insideNestedFunction = true;
assert(topLevel);
assert(insideMain);
assert(insideFunction);
assert(insideNestedFunction);
}
}
}
nestedFunction() 메서드는 최상위까지 포함해 모든 레벨의 변수를 사용할 수 있어요.
어휘적 클로저 (Lexical closures)
자신의 어휘적 유효 범위 밖에 있을 때에도 그 유효 범위의 변수에 접근할 수 있는 함수 객체를 *클로저(closure)*라고 해요.
함수는 주변 유효 범위에 정의된 변수를 닫아 둘 수 있어요(closing over). 아래 예시에서 makeAdder()는 addBy 변수를 캡처해요. 반환된 함수가 어디로 가든 addBy를 기억하죠.
/// Returns a function that adds [addBy] to the
/// function's argument.
Function makeAdder(int addBy) {
return (int i) => addBy + i;
}
void main() {
// Create a function that adds 2.
var add2 = makeAdder(2);
// Create a function that adds 4.
var add4 = makeAdder(4);
assert(add2(3) == 5);
assert(add4(3) == 7);
}
티어오프 (Tear-offs)
괄호 없이 함수·메서드·이름 있는 생성자를 참조하면 Dart는 *티어오프(tear-off)*를 만들어요. 이는 그 함수와 같은 파라미터를 받고, 호출할 때 기저 함수를 호출하는 클로저예요. 코드가 클로저가 받는 것과 같은 파라미터로 이름 있는 함수를 호출하는 클로저를 필요로 한다면, 호출을 람다로 감싸지 말고 티어오프를 쓰세요.
var charCodes = [68, 97, 114, 116];
var buffer = StringBuffer();
좋은 예(good):
// Function tear-off
charCodes.forEach(print);
// Method tear-off
charCodes.forEach(buffer.write);
나쁜 예(bad):
// Function lambda
charCodes.forEach((code) {
print(code);
});
// Method lambda
charCodes.forEach((code) {
buffer.write(code);
});
함수의 동등성 검사 (Testing functions for equality)
최상위 함수, 정적 메서드, 인스턴스 메서드의 동등성을 검사하는 예시를 볼게요.
void foo() {} // A top-level function
class A {
static void bar() {} // A static method
void baz() {} // An instance method
}
void main() {
Function x;
// Comparing top-level functions.
x = foo;
assert(foo == x);
// Comparing static methods.
x = A.bar;
assert(A.bar == x);
// Comparing instance methods.
var v = A(); // Instance #1 of A
var w = A(); // Instance #2 of A
var y = w;
x = w.baz;
// These closures refer to the same instance (#2),
// so they're equal.
assert(y.baz == x);
// These closures refer to different instances,
// so they're unequal.
assert(v.baz != w.baz);
}
반환 값 (Return values)
모든 함수는 값을 반환해요. 반환 값을 지정하지 않으면 return null; 문장이 함수 본문에 암묵적으로 추가돼요.
foo() {}
assert(foo() == null);
함수에서 여러 값을 반환하려면 레코드(record)에 값을 모으면 돼요.
(String, int) foo() {
return ('something', 42);
}
게터와 세터 (Getters and setters)
모든 프로퍼티 접근(최상위, 정적, 인스턴스)은 게터나 세터의 호출이에요. 변수는 암묵적으로 게터를 만들고, mutable이라면 세터도 만들어요. 그래서 프로퍼티에 접근할 때 실제로는 배경에서 작은 함수를 호출하고 있는 거예요. 프로퍼티를 읽으면 게터 함수를 호출하고, 쓰면 세터 함수를 호출하는 거죠. 프로퍼티가 변수로 선언된 경우에도 마찬가지예요.
하지만 게터와 세터를 get과 set 키워드로 각각 명시적으로 선언할 수도 있어요. 이렇게 하면 프로퍼티 값을 읽거나 쓸 때 그 값을 계산할 수 있어요.
게터와 세터를 쓰는 목적은 클라이언트(프로퍼티를 사용하는 코드)와 제공자(그것을 정의하는 클래스나 라이브러리) 사이에 분명한 분리를 만드는 거예요. 클라이언트는 그 값이 단순 변수에 저장되어 있는지, 아니면 그 자리에서 계산되는지 알 필요 없이 값을 요청하거나 설정해요. 덕분에 제공자는 프로퍼티가 동작하는 방식을 자유롭게 바꿀 수 있어요.
예를 들어 프로퍼티의 값이 어디에도 저장되지 않을 수 있으므로 게터가 호출될 때마다 계산될 수 있어요. 또 다른 예로, 값이 비공개 변수에 저장되어 있고 공개 접근은 게터나 세터 호출로만 허용되는 경우가 있어요.
아래 예시가 이를 보여줘요. secret 게터와 세터가 할당·검색된 값에 각자의 처리를 더해 비공개 변수 _secret에 간접 접근을 제공해요.
// Defines a variable `_secret` that is private to the library since
// its identifier starts with an underscore (`_`).
String _secret = 'Hello';
// A public top-level getter that
// provides read access to [_secret].
String get secret {
print('Getter was used!');
return _secret.toUpperCase();
}
// A public top-level setter that
// provides write access to [_secret].
set secret(String newMessage) {
print('Setter was used!');
if (newMessage.isNotEmpty) {
_secret = newMessage;
print('New secret: "$newMessage"');
}
}
void main() {
// Reading the value calls the getter.
print('Current message: $secret');
/*
Output:
Getter was used!
Current message: HELLO
*/
// Assigning a value calls the setter.
secret = 'Dart is fun';
// Reading it again calls the getter to show the new computed value
print('New message: $secret');
/*
Output:
Setter was used! New secret: "Dart is fun"
Getter was used!
New message: DART IS FUN
*/
}
제너레이터 (Generators)
값의 시퀀스를 지연(lazily) 생성해야 한다면 *제너레이터 함수(generator function)*를 고려해 보세요. Dart는 두 종류의 제너레이터 함수를 내장 지원해요.
동기 제너레이터 함수를 구현하려면 함수 본문을 sync*로 표시하고, yield 문으로 값을 전달하면 돼요.
Iterable<int> naturalsTo(int n) sync* {
int k = 0;
while (k < n) yield k++;
}
비동기 제너레이터 함수를 구현하려면 함수 본문을 async*로 표시하고, yield 문으로 값을 전달하면 돼요.
Stream<int> asynchronousNaturalsTo(int n) async* {
int k = 0;
while (k < n) yield k++;
}
제너레이터가 재귀적이라면 yield*를 사용해 성능을 개선할 수 있어요.
Iterable<int> naturalsDownFrom(int n) sync* {
if (n > 0) {
yield n;
yield* naturalsDownFrom(n - 1);
}
}
외부 함수 (External functions)
외부 함수(external function)는 본문이 선언과 분리되어 구현되는 함수예요. 함수 선언 앞에 external 키워드를 붙이면 돼요.
external void someFunc(int i);
외부 함수의 구현은 다른 Dart 라이브러리에서, 더 일반적으로는 다른 언어에서 올 수 있어요. 인터럽(interop) 맥락에서 external은 외부 함수나 값에 대한 타입 정보를 제공해서 Dart에서 사용할 수 있게 해 줘요. 구현과 사용은 플랫폼에 크게 의존하므로, 예를 들어 C나 JavaScript에 대한 인터럽 문서를 확인해 더 배워 보세요.
외부 함수는 최상위 함수, 인스턴스 메서드, 게터나 세터, 또는 non-redirecting 생성자가 될 수 있어요. 인스턴스 변수도 external일 수 있는데, 이는 외부 게터와 (변수가 final이 아니라면) 외부 세터와 동등해요.
더 알아보기 (Learn more)
- Records — 여러 값 반환하기
- Callable objects — 인스턴스를 함수처럼 호출하기
- Asynchronous programming — 비동기 처리와 스트림