컴파일러 지시어
컴파일러 지시어 (Compiler Directives)
F# 컴파일러가 소스 코드를 처리하는 방식을 제어하는 지시어들을 살펴볼게요. #if, #else, #endif 같은 조건부 컴파일 지시어부터 경고를 끄고 켜는 #nowarn/#warnon, 디버깅용 줄 정보를 알려주는 #line까지, F# 코드에서 꼭 알아두면 좋은 기능들을 정리해 드릴게요.
출처: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/compiler-directives
본문
이 문서에서는 컴파일러 지시어에 대해 다룹니다. F# Interactive의 지시어(예: dotnet fsi)가 궁금하다면 Interactive Programming with F# 을 참고하세요.
컴파일러 지시어는 # 기호로 시작하며, 한 줄을 단독으로 차지해야 합니다.
다음 표는 F#에서 사용할 수 있는 컴파일러 지시어 목록입니다.
| 지시어 | 설명 |
|---|---|
#if if-expression |
조건부 컴파일을 지원합니다. #if 다음의 코드는 해당 if-expression이 defined로 평가될 때만 포함됩니다 (아래 설명 참고). |
#else |
조건부 컴파일을 지원합니다. 이전 #if에 사용된 심볼이 defined로 평가되지 않을 때 포함할 코드 구간을 표시합니다. |
#endif |
조건부 컴파일을 지원합니다. 조건부 코드 구간의 끝을 표시합니다. |
#[line] int, #[line] int string, #[line] int verbatim-string |
디버깅을 위해 원본 소스 코드의 줄 번호와 파일 이름을 알려줍니다. 이 기능은 F# 소스 코드를 생성하는 도구를 위해 제공됩니다. |
#nowarn warningcodes |
warningcodes로 지정된 하나 이상의 컴파일러 경고를 비활성화합니다 (아래 설명 참고). |
#warnon warningcodes |
warningcodes로 지정된 하나 이상의 컴파일러 경고를 활성화합니다 (아래 설명 참고). |
조건부 컴파일 지시어
이 지시어들에 의해 비활성화된 코드는 Visual Studio 코드 편집기에서 흐리게(디밍) 표시됩니다.
다음 코드는 #if, #else, #endif 지시어의 사용 예를 보여줍니다. 이 예시에서 function1의 정의가 두 가지 버전으로 존재합니다. -define 컴파일러 옵션으로 VERSION1이 정의되면 #if 지시어와 #else 지시어 사이의 코드가 활성화됩니다. 그렇지 않으면 #else와 #endif 사이의 코드가 활성화됩니다.
#if VERSION1
let function1 x y =
printfn "x: %d y: %d" x y
x + 2 * y
#else
let function1 x y =
printfn "x: %d y: %d" x y
x - 2*y
#endif
let result = function1 10 20
#if 지시어는 논리 표현식도 받아들입니다:
#if SILVERLIGHT || COMPILED && (NETCOREFX || !DEBUG)
#endif
다음 표현식들을 사용할 수 있습니다.
| if-expr | 평가 결과 |
|---|---|
if-expr1 || if-expr2 |
if-expr1 또는 if-expr2 중 하나가 defined이면 defined. |
if-expr1 && if-expr2 |
if-expr1과 if-expr2가 모두 defined이면 defined. |
!if-expr1 |
if-expr1이 defined가 아니면 defined. |
( if-expr1 ) |
if-expr1이 defined이면 defined. |
symbol |
-define 컴파일러 옵션으로 정의된 것으로 표시되면 defined. |
논리 연산자는 일반적인 논리 연산자 우선순위를 따릅니다.
하지만 F#에는 #define 컴파일러 지시어가 없습니다. #if 지시어가 사용하는 심볼을 정의하려면 컴파일러 옵션이나 프로젝트 설정을 사용해야 합니다.
조건부 컴파일 지시어는 중첩할 수 있습니다. 컴파일러 지시어에서는 들여쓰기가 중요하지 않습니다.
미리 정의된 심볼 (Predefined symbols)
F# 컴파일러와 빌드 시스템은 조건부 컴파일에 사용할 수 있는 여러 심볼을 자동으로 정의합니다.
빌드 구성 심볼 (Build configuration symbols)
다음 심볼들은 빌드 구성에 따라 정의됩니다:
-
DEBUG: Debug 모드로 컴파일할 때 정의됩니다. 프로젝트 시스템에서DEBUG심볼은 Debug 구성에서 자동으로 정의되지만, Release 구성에서는 정의되지 않습니다. 이 심볼은 주로 어서션(assertion)이나 진단 코드와 함께 사용됩니다. 자세한 내용은 Assertions 을 참고하세요. -
TRACE: 추적(tracing)을 활성화한 빌드에 대해 정의됩니다.DEBUG와 마찬가지로 보통 Debug 구성에서 정의되지만, Release 구성에서도 활성화할 수 있습니다.
이 값들은 -define 컴파일러 옵션이나 프로젝트 설정을 통해 재정의할 수 있습니다.
컴파일 모드 심볼 (Compilation mode symbols)
다음 심볼들은 서로 다른 컴파일 모드를 구분합니다:
-
COMPILED: F# 컴파일러로 코드를 컴파일할 때 정의됩니다. 컴파일된 어셈블리와 F# Interactive 세션에서 코드가 다르게 동작하도록 해야 할 때 유용합니다. -
INTERACTIVE: F# Interactive (dotnet fsi)에서 코드를 컴파일하거나 실행할 때 정의되는데, 여기에는 대화형 세션(interactive session)과 스크립트 실행이 모두 포함됩니다. 이 심볼을 이용하면 대화형으로 실행할 때 다르게 동작하는 코드를 작성할 수 있습니다.
스크립트에서 이 심볼들을 사용하는 방법에 대한 자세한 내용은 Interactive Programming with F# 을 참고하세요.
예시:
#if INTERACTIVE
// Code specific to F# Interactive
#r "nuget: Newtonsoft.Json"
#endif
#if COMPILED
// Code specific to compiled assemblies
open System.Configuration
#endif
대상 프레임워크 심볼 (Target framework symbols)
빌드 시스템은 SDK 스타일 프로젝트에서 다양한 대상 프레임워크에 대한 전처리기 심볼도 정의합니다. 이러한 심볼은 여러 .NET 버전을 대상으로 하는 라이브러리나 애플리케이션을 만들 때 유용합니다.
| 대상 프레임워크 | 심볼 | 추가 심볼 (.NET 5+ SDK 사용 가능) | 플랫폼 심볼 (OS 특정 TFM을 지정할 때만 사용 가능) |
|---|---|---|---|
| .NET Framework | NETFRAMEWORK, NET481, NET48, NET472, NET471, NET47, NET462, NET461, NET46, NET452, NET451, NET45, NET40, NET35, NET20 |
NET48_OR_GREATER, NET472_OR_GREATER, NET471_OR_GREATER, NET47_OR_GREATER, NET462_OR_GREATER, NET461_OR_GREATER, NET46_OR_GREATER, NET452_OR_GREATER, NET451_OR_GREATER, NET45_OR_GREATER, NET40_OR_GREATER, NET35_OR_GREATER, NET20_OR_GREATER |
|
| .NET Standard | NETSTANDARD, NETSTANDARD2_1, NETSTANDARD2_0, NETSTANDARD1_6, NETSTANDARD1_5, NETSTANDARD1_4, NETSTANDARD1_3, NETSTANDARD1_2, NETSTANDARD1_1, NETSTANDARD1_0 |
NETSTANDARD2_1_OR_GREATER, NETSTANDARD2_0_OR_GREATER, NETSTANDARD1_6_OR_GREATER, NETSTANDARD1_5_OR_GREATER, NETSTANDARD1_4_OR_GREATER, NETSTANDARD1_3_OR_GREATER, NETSTANDARD1_2_OR_GREATER, NETSTANDARD1_1_OR_GREATER, NETSTANDARD1_0_OR_GREATER |
|
| .NET 5+ (및 .NET Core) | NET, NET10_0, NET9_0, NET8_0, NET7_0, NET6_0, NET5_0, NETCOREAPP, NETCOREAPP3_1, NETCOREAPP3_0, NETCOREAPP2_2, NETCOREAPP2_1, NETCOREAPP2_0, NETCOREAPP1_1, NETCOREAPP1_0 |
NET10_0_OR_GREATER, NET9_0_OR_GREATER, NET8_0_OR_GREATER, NET7_0_OR_GREATER, NET6_0_OR_GREATER, NET5_0_OR_GREATER, NETCOREAPP3_1_OR_GREATER, NETCOREAPP3_0_OR_GREATER, NETCOREAPP2_2_OR_GREATER, NETCOREAPP2_1_OR_GREATER, NETCOREAPP2_0_OR_GREATER, NETCOREAPP1_1_OR_GREATER, NETCOREAPP1_0_OR_GREATER |
ANDROID, BROWSER, IOS, MACCATALYST, MACOS, TVOS, WINDOWS, [OS][version] (예: IOS15_1), [OS][version]_OR_GREATER (예: IOS15_1_OR_GREATER) |
참고:
-
버전이 없는 심볼은 대상 버전과 무관하게 항상 정의됩니다.
-
버전이 있는 심볼은 대상으로 하는 버전에 대해서만 정의됩니다.
-
<framework>_OR_GREATER심볼은 대상 버전과 그보다 낮은 모든 이전 버전에 대해 정의됩니다. 예를 들어 .NET Framework 2.0을 대상으로 한다면 다음 심볼들이 정의됩니다:NET20,NET20_OR_GREATER,NET11_OR_GREATER,NET10_OR_GREATER. -
NETSTANDARD<x>_<y>_OR_GREATER심볼은 .NET Standard 대상에 대해서만 정의되며, .NET Core나 .NET Framework 같이 .NET Standard를 구현하는 대상에는 정의되지 않습니다. -
이 심볼들은 MSBuild
TargetFramework속성과 NuGet에서 사용하는 대상 프레임워크 모니커(TFM)와는 다릅니다.
예를 들어, 대상 프레임워크에 따라 조건부 컴파일을 하려면 이렇게 사용합니다:
#if NET6_0_OR_GREATER
// Use .NET 6+ specific APIs
#else
// Use alternative implementation for older frameworks
#endif
NULLABLE 지시어
F# 9부터 프로젝트에서 nullable 참조 타입을 활성화할 수 있습니다:
<Nullable>enable</Nullable>
이렇게 하면 빌드에 NULLABLE 지시어가 자동으로 설정됩니다. 이 기능을 도입하면서 충돌이 생기는 코드를 #if NULLABLE 해시 지시어로 조건부로 바꾸고 싶을 때 유용합니다:
#if NULLABLE
let length (arg: 'T when 'T: not null) =
Seq.length arg
#else
let length arg =
match arg with
| null -> -1
| s -> Seq.length s
#endif
줄 지시어 (Line Directives)
빌드할 때 컴파일러는 F# 코드의 오류를 각 오류가 발생한 줄 번호를 참조해 보고합니다. 이 줄 번호는 파일의 첫 줄부터 1로 시작합니다. 그런데 다른 도구로 F# 소스 코드를 생성한다면, 생성된 코드의 줄 번호는 대개 관심이 없습니다. 생성된 F# 코드의 오류는 대부분 다른 원인에서 비롯되기 때문이죠. #line 지시어는 F# 소스 코드를 생성하는 도구 작성자가 원본 줄 번호와 소스 파일 정보를 생성된 F# 코드에 전달할 수 있게 해줍니다.
#line 지시어를 사용할 때 파일 이름은 따옴표로 묶어야 합니다. 문자열 앞에 verbatim 토큰(@)이 없다면, 경로에 백슬래시를 사용하려면 백슬래시 하나 대신 두 개로 이스케이프해야 합니다. 다음은 유효한 줄 토큰 예시들입니다. 이 예시들에서는 원본 파일 Script1이 도구를 거쳐 자동 생성된 F# 코드 파일이 되고, 이 지시어들이 있는 위치의 코드가 파일 Script1의 25번째 줄에 있는 어떤 토큰들로부터 생성되었다고 가정합니다.
# 25
#line 25
#line 25 "C:\\Projects\\MyProject\\MyProject\\Script1"
#line 25 @"C:\Projects\MyProject\MyProject\Script1"
# 25 @"C:\Projects\MyProject\MyProject\Script1"
이 토큰들은 이 위치에서 생성된 F# 코드가 Script1의 25번째 줄 부근에 있는 어떤 구성 요소에서 파생되었음을 나타냅니다.
#line 지시어는 #nowarn/#warnon의 동작에는 영향을 주지 않는다는 점을 유의하세요. 이 두 지시어는 항상 컴파일되고 있는 파일을 기준으로 합니다.
경고 지시어 (Warn Directives)
Warn 지시어는 소스 파일의 특정 부분에 대해 지정된 컴파일러 경고를 비활성화하거나 활성화합니다.
warn 지시어는 한 줄의 소스 코드로, 다음과 같은 구성요소로 이루어집니다:
-
선택적인 앞쪽 공백(whitespace)
-
#nowarn또는#warnon문자열 -
공백
-
공백으로 구분된 하나 이상의 warningcode (아래 참고)
-
선택적인 공백
-
선택적인 줄 주석(line comment)
warningcode는 숫자 나열(경고 번호를 나타냄)이며, 선택적으로 FS가 앞에 붙거나, 선택적으로 큰따옴표로 둘러쌀 수 있습니다.
#nowarn 지시어는 같은 경고 번호에 대한 #warnon 지시어가 나타나거나 파일 끝에 도달할 때까지 해당 경고를 비활성화합니다. 마찬가지로 #warnon 지시어는 같은 경고 번호에 대한 #nowarn 지시어가 나타나거나 파일 끝에 도달할 때까지 해당 경고를 활성화합니다. 이러한 쌍의 앞과 뒤에서는 컴파일 기본값이 적용되는데, 그 내용은 다음과 같습니다:
-
--nowarn컴파일러 옵션(또는 해당 MSBuild 속성)으로 비활성화되었으면 경고가 표시되지 않습니다. -
--warnon컴파일러 옵션(또는 해당 MSBuild 속성)으로 활성화되지 않았다면 opt-in 경고는 표시되지 않습니다.
다음은 (다소 억지스러운) 예시입니다.
module A
match None with None -> () // warning
let x =
#nowarn 25
match None with None -> 1 // no warning
#warnon FS25
match None with None -> () // warning
#nowarn "FS25" FS007 "42"
match None with None -> () // no warning