upvar — 다른 스택 프레임의 변수에 연결 고리 만들기

upvar — 다른 스택 프레임의 변수에 연결 고리 만들기

함수에 변수를 전달할 때 값을 복사로 넘기기보다, 호출한 쪽의 변수에 직접 연결하고 싶다면 upvar를 써요. 이걸로 call-by-name 방식의 프로시저 호출을 간단히 구현할 수 있고, 새 제어 구조를 Tcl 프로시저로 만들기도 쉬워져요. 트레이스와 결합할 때는 살짝 예상 밖의 동작이 있어서 그 부분을 함께 알아둘 필요가 있어요.

출처: 문서

본문

문법

upvar ?level? otherVar myVar ?otherVar myVar ...?

이 명령어는 현재 프로시저의 하나 이상의 지역 변수가, 감싸고 있는 프로시저 호출이나 전역 변수를 가리키도록 정리해줘요.

leveluplevel 명령어가 허용하는 형태 중 아무 것이나 가질 수 있고, 생략해도 돼요(기본값은 1).

otherVar 인자에 대해, upvarlevel이 주어진 프로시저 프레임(또는 level#0이면 전역 레벨)의 그 이름을 가진 변수가 현재 프로시저에서 대응하는 myVar 이름으로 접근 가능하게 만들어요.

otherVar가 가리키는 변수는 호출 시점에 존재할 필요가 없어요. myVar를 처음 참조할 때 일반 변수처럼 그때 만들어져요. upvar가 호출되는 시점에 myVar라는 이름의 변수가 이미 존재하면 안 돼요.

myVar는 항상 변수 이름으로 취급되고, 배열 원소로는 취급되지 않아요. 이름이 a(b) 같은 배열 원소처럼 생기면 오류가 반환돼요. otherVar는 스칼라 변수, 배열, 배열 원소를 가리킬 수 있어요.

upvar는 빈 문자열을 반환해요.

upvar 명령어는 call-by-name 방식의 프로시저 호출 구현을 단순화하고, 새 제어 구조를 Tcl 프로시저로 만들기도 쉽게 해줘요. 다음 프로시저를 생각해볼게요:

proc add2 name {
    upvar $name x
    set x [expr {$x + 2}]
}

add2가 변수 이름을 인자로 주고 호출되면, 그 변수의 값에 2를 더해요. add2uplevel 대신 upvar로 구현했을 때 더 간단한 이유는, upvar가 호출자 프로시저 프레임의 변수에 접근하기 더 쉽게 만들어주기 때문이에요.

namespace eval은 (프로시저 호출 외에) Tcl 이름 문맥을 바꾸는 또 다른 방법이에요. 네임스페이스 문맥을 나타내기 위해 스택에 호출 프레임을 추가해요. 즉 각 namespace eval 명령어는 uplevelupvar 명령어에게 또 하나의 호출 레벨로 계산돼요. 예를 들어 info level 1은 맨 바깥 프로시저 호출이거나 맨 바깥 namespace eval 명령어인 명령어를 설명하는 리스트를 반환해요. 또한 uplevel #0은 맨 바깥 네임스페이스(전역 네임스페이스)의 최상위에서 스크립트를 평가해요.

upvar 변수를 unset하면(위 add2x처럼), 그 unset 연산은 upvar 변수 자체가 아니라 연결된 변수에 영향을 줘요. upvar 변수는 그것을 정의한 프로시저를 나갈 때를 제외하고는 unset할 방법이 없어요. 다만 다른 upvar 명령어를 실행해 upvar 변수를 다시 대상으로 지정(retarget)하는 것은 가능해요.

트레이스와 upvar

upvar는 트레이스와 직관적이면서도 어쩌면 예상 밖의 방식으로 상호작용해요. otherVar에 변수 트레이스가 정의돼 있으면, 그 트레이스는 myVar를 다루는 동작에 의해 촉발돼요. 하지만 트레이스 프로시저에는 otherVar의 이름이 아니라 myVar의 이름이 전달돼요. 따라서 다음 코드의 출력은 originalVar이 아니라 localVar이에요:

proc traceproc { name index op } {
    puts $name
}
proc setByUpvar { name value } {
    upvar $name localVar
    set localVar $value
}
set originalVar 1
trace variable originalVar w traceproc
setByUpvar originalVar 2

otherVar가 배열의 한 원소를 가리키면, myVar에 접근할 때 배열 전체에 설정된 변수 트레이스는 호출되지 않아요(하지만 특정 원소의 트레이스는 여전히 호출돼요). 특히 배열이 env라면 myVar에 가한 변경이 하위 프로세스에 제대로 전달되지 않아요.

예제

incr처럼 동작하되 값을 더하는 대신 빼는 decr 명령어:

proc decr {varName {decrement 1}} {
    upvar 1 $varName var
    incr var [expr {-$decrement}]
}

더 알아보기

  • global, namespace, uplevel, variable — 변수·문맥 관련 명령어
  • 관련 키워드: context, frame, global, level, namespace, procedure, upvar, variable