아이템 1: 지역변수를 선언할 때는 var를 사용하는 것이 낫다.
지역변수의 타입을 암시적으로 선언하는 것이 좋은 이유는 C# 언어가 익명 타입을 지원하기 위해서 타입을 암시적으로 선언할 수 있는 손쉬운 방법을 제공하기 때문이다.
정확한 반환 타입을 알지 못한 채 올바르지 않은 타입을 명시적으로 지정하면 득보다 실이 많다.
IQueryable<T> 컬렉션을 IEnumerable<T>로 강제 형변환하게 되면 IQueryProvider가 제공하는 장점을 모두 잃게 된다.
var를 사용하면 개발자 입장에서는 변수의 타입과 같이 지엽적인 부분보다 변수의 의미 파악에 더 집중할 수 있다. 컴파일러 관점에서 살펴본다면 개발자가 특정 객체의 타입을 명시적으로 지정하지 않더라도 객체 생성에 문제가 있을 경우 오류를 보고할 수 있다.
타입을 명시적으로 지정할 경우 타입 안정성이 향상될 것이라 생각하지만 이 또한 사실이 아니다. 앞서 IQueryable<T>와 IEnumerable<T>의 예와 같이 개발자가 올바르게 타입을 지정하지 않으면 오히려 타입 안정성을 해치는 꼴이 될 수도 있다.
C#에서 특정 변수를 var로 선언하면 동적 타이핑이 수행되는 것이 아니라 할당 연산자 오른쪽의 타입을 확인하여 왼쪽 변수의 타입을 결정하게 된다. 컴파일러에게 변수의 타입을 명시적으로 알려주지 않아도 개발자를 대신하여 올바른 타입을 추론해 주는 것이다.
var를 과도하게 사용한 나머지 코드의 가독성을 해치는 경우도 있을 수 있다.
var thing = AccountFactory.CreateSavingAccount();
var result = someObject.DoSomeWork(anotherParameter);
실제 코드 작성 시에는 이보다 훨씬 명확하게 메서드의 이름을 지을 것이며 또한 반드시 그래야 한다.
var HighesetSellingProduct = someObject.DoSomeWork(anotherParameter);
이 코드에는 타입과 관련된 정보가 없지만 HighestSellingProduct라는 변수 이름을 통해서 Product 타입임을 미루어 짐작할 수 있다. 변수의 이름을 통해서 그 역할을 명확하게 드러내도록 코드를 작성해야 한다.
내장 숫자 타입과 var
C#이 제공하는 내장 숫자 타입들은 매우 다양한 형변환 기능을 가지고 있고 정밀도도 각기 다르다. 이로 인해 숫자 타입과 var를 함께 사용하면 가독성 문제뿐 아니라 정밀도와 관련된 혼돈스러운 문제를 유발할 가능성이 있다.
var f = GetMagicNumber();
var total = 100 * f / 6;
total은 무슨 타입일까? total의 정확한 타입은 GetMagicNumber() 메서드의 반환 타입에 따라 결정된다.
total의 타입을 명시적으로 선언하여 문제를 해결한다.
double total = 100 * GetMagicNumber() / 6;
코드를 읽을 때 타입을 명시적으로 드러내야 하는 경우가 아니라면 var를 사용하는 것이 좋다. 다만 내장 숫자 타입(int, float, double 등)을 선언할 때는 명시적으로 타입을 선언하는 편이 낫다. 어느 경우라도 변수/메서드의 이름을 타입/역할이 명확하게 드러나도록 지정하는 것이 좋다.
아이템 2: const보다는 readonly가 좋다
C#은 컴파일타임 상수와 런타임 상수 두 유형의 상수를 가진다. 컴파일타임 상수가 약간 더 빠르긴 하지만 런타임 상수에 비해 유연성이 상당히 떨어진다. 컴파일타임 상수는 성능이 매우 중요하고 상수의 값이 절대로 바뀌지 않는 경우에만 제한적으로 사용하는 것이 좋다.
// 컴파일타임 상수
public const int Millennium = 2000;
// 런타임 상수
public static readonly int ThisYear = 2004;
컴파일타임 상수는 메서드 내부에서도 선언할 수 있지만, 런타임 상수는 메서드 내에서는 선언할 수 없다.
런타임 상수와 컴파일타임 상수가 서로 다르게 동작하는 이유는 값에 접근하는 방법이 서로 다르기 때문이다.
컴파일타임 상수는 컴파일타임에 값으로 대체되는 반면, 런타임 상수는 상수에 대한 참조로 컴파일 되고, 런타임에 값이 평가된다.
if (myDateTime.Year == Millenium)
...
// 위 코드는 아래와 동일한 코드로 컴파일된다.
if (myDateTime.Year == 2000)
...
컴파일타임 상수는 내장된 숫자형, enum, 문자열, null에 대해서만 사용될 수 있다. 내장 자료형이어야만 컴파일타임에 상수를 리터럴로 대체할 수 있기 때문이다.
// 컴파일되지 않는다. 대신 readonly를 사용해야 한다.
private const DateTime classCreation = new DateTime(2000, 1, 1, 0, 0, 0);
런타임 상수는 생성자를 통해서도 초기화될 수 있으며, 어떤 타입과도 함께 사용도리 수 있다.
클래스 내에서 런타임 상수를 정의하는 경우라면 동일 클래스의 인스턴스라도 인스턴스별로 서로 다른 값을 가질 수 있다. 반면 컴파일타임 상수는 정의에 따라 정적 상수이므로 모든 인스턴스가 동일한 값을 가진다.
컴파일타임 상수와 런타임 상수의 가장 중요한 차이는 런타임 상수는 상수의 값이 런타임에 평가된다는 점이다. 컴파일타임 상수의 값을 수정하게 되면 해당 상수를 참조하는 모든 코드를 반드시 재컴파일해야 한다. 하지만 런타임 상수를 사용하는 경우에는 값을 변경하는 것만으로 족하다.
readonly 대신 const를 사용했을 때 얻는 장점은 성능이 빠르다는 것이다. 하지만 이를 통해 얻을 수 있는 성능 개선 효과가 크지 않고 무엇보다 유연성을 해치는 단점이 있다.
명명된 매개변수(named parameter) 나 선택적 매개변수(optional parameter)도 앞서 알아본 상수의 특성과 연관이 있다. 선택적 매개변수에 대한 기본값은 컴파일타임 상수의 형태로 메서드를 사용하는 호출 측에 저장된다. 따라서 선택적 매개변수의 값을 변경할 때도 const 변수와 마찬가지로 매우 신중하게 접근해야 한다.
그 외에 컴파일할 때 사용되는 상숫값을 정의할 때는 반드시 const를 사용해야 한다. 특성(attribute)의 매개변수, switch/case 문의 레이블, enum 정의 시 사용하는 상수 등은 컴파일 시에 사용돼야 하므로 반드시 const를 통해 초기화돼야 한다.
몇 가지 예외적인 상황을 제외한다면 대부분의 경우 const보다는 readonly를 사용하는 것이 좋다.
아이템 3: 캐스트보다는 is, as가 좋다.
as를 사용하는 편이 캐스트보다 더 안전하고 런타임에 더 효율적으로 동작한다.
예시)
object c = Factory.GetObject();
MyType t = o as MyType;
if (t != null)
{
... // MyType t 객체 사용
}
else
{
... // 오류 보고
}
object o = Factory.GetObject();
try
{
MyType = t;
t = (MyType) o;
// MyType t 사용
}
catch (InvalidCastException)
{
// 오류 보고
}
첫 번째 코드가 읽고 쓰기 편하다. try/catch 문이 없어 성능도 좋다. 캐스팅을 사용하더라도 null은 어떤 참조 타입으로도 형변환 될 수 있기 때문에 여전히 null 체크를 해야 한다.
as 연산자와 캐스팅의 가장 큰 차이는 사용자 정의 형변환을 다루는 방식이다. as나 is 연산자를 사용하면 사용자 정의 형변환은 수행되지 않는다. 사용자 정의 형변환 연산자는 객체의 런타임 타입이 아닌 컴파일타임 타입에 맞춰 수행된다.
public class SecondType
{
private MyType _value;
public static implicit operator MyType(SecondType t)
{
return t._value;
}
}
object o = Factory.GetObject(); // public SecondType GetObejct()
MyType t = o as MyType; // 첫 번째 방법
MyType t2 = (MyType) o; // 두 번째 방법
위 두 방법은 모두 형변환에 실패한다. 캐스팅을 사용하면 사용자 정의 형변환 연산자는 사용되지만, 컴파일러는 런타임에 객체가 어떤 타입일지 예측하지 못한다. 컴파일러는 단순히 컴파일타임에 객체가 어떤 타입으로 선언됐는지만 추적하기 때문에 두 번째 방법 또한 실패한다.
컴파일러는 객체 o가 object 타입이라 생각하고, MyType 형식인지 확인하는 코드만 생성한다. 불행히도 런타임에 o는 SecondTyp 형식의 객체이므로 형변환에 실패한다.
.NET 기본 클래스 라이브러리에는 시퀀스 내의 개별 요소들을 특정 타입으로 형변환하는 Enumerable.Cast<T>()와 같은 함수가 있다. 이 함수는 IEnumerable 인터페이스만을 지원하는 컬렉션에 포함된 각각의 객체에 대해 형변환을 수행할 때 주로 사용한다.
IEnumerable collection = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var small = from int item in collection
where item < 5
select item;
var small2 = collection.Cast<int>().Where(item => item < 5).Select(n => n);
위 두 방식은 모두 형변환을 위해 Cast<T> 메서드를 호출하고, 동일한 코드를 생산한다. Enumerable.Cast<T> 메서드는 as 연산자 대신 캐스트 연산을 사용하는데, as 연산자를 사용하면 형변환하려는 타입에 제한이 생기기 때문이다.
제네릭 컬렉션에 대해서는 Cast<>를 호출할 수 없다. int형 시퀀스에 대해 Cast<double>()을 수행하면 실패한다.
객체 지향 프로그래밍에서는 가능하면 형변환을 피하는 것이 좋다. 하지만 불가피한 경우 사용자의 의도를 명확히 표현할 수 있는 is, as 연산자를 사용하라.
아이템 4: string.Format()을 보간 문자열로 대체하라.
C#에 새롭게 도입된 문자열 보간 기능
string.Format()은 포맷 문자열과 인자 리스트를 분리하여 전달하는 구조다. 결과를 출력해보고 올바른 형태를 눈으로 직접 확인해야 하기 때문에 코드를 제대로 작성했는지 알기 어렵다. 또한 포맷 문자열에 나타낸 인자의 개수와 실제로 전달되는 인자의 개수가 정확히 일치하는지 확인하지 않아 런타임에 예외가 발생할 수 있다. 인자의 순서가 올바른지 확인하기도 어렵다.
보간 문자열은 문자열 앞에 '$'를 붙여서 사용한다. 문자열로 변경할 표현식은 {}내에 둔다. 이를 대체 문자열이라 하는데 이런 형식 덕분에 가독성이 상당히 좋다.
사용자가 문자열 보간 기능을 사용하더라도 실제 C# 컴파일러는 param을 이용하여 object 배열을 전달하는 기존 포매팅 함수를 호출하도록 코드를 생성한다. 값 타입의 사용할 경우 object 타입으로 변경하기 위해 박싱을 수행해야 한다. 따라서 전달할 인자를 사전에 문자열로 변경하여 값 타입의 박싱을 피하는 것이 좋다.
Console.WriteLine($"The value of pi is {Math.PI.ToString()}");
아이템 5: 문화권별로 다른 문자열을 생성하려면 FormattableString을 사용하라
아이템 4: 보간 문자열 기능의 결과로 생성되는 반환값은 string일 수도 있지만 FormattableString을 상속한 타입일 수도 있다.
string first = $"It's the {DateTime.Now.Day} of the {DateTime.Now.Month} month";
FormattableString second = $"It's the {DateTime.Now.Day} of the {DateTime.Now.Month} month";
만약 아래와 같이 var로 선언하면 이 변수는 string 객체가 될 수도 있겠지만, FormattableString을 상속한 타입의 객체가 될 수도 있다.
만약 FormattableString을 상속한 타입의 객체라면 컴퓨터에 지정된 문화권을 고려하여 미국에선 소수점 기호 '.', 유럽에선 소수점 기호 ','가 사용된다.
var third = $"It's the {DateTime.Now.Day} of the {DateTime.Now.Month} month";
FormattableString 타입의 객체를 활용하면 문화권과 언어를 지정하여 문자열을 생성할 수 있다.
public static string ToGerman(FormattableString src)
{
return string.Format(null, System.Globalization.CultureInfo.CreateSpecificCulture("de-de"),
src.Format, src.GetArguments());
}
public static string ToFrenchCanada(FormattableString src)
{
return string.Format(null, System.Globalization.CultureInfo.CreateSpecificCulture("fr-CA"),
src.Format, src.GetArguments());
}
문화권을 임의로 지정해야 하는 경우에는 명시적으로 FormattableString 타입의 객체를 생성하도록 코드를 작성하고 이 객체를 통해 문자열을 얻어 오는 방법을 사용하는 것이 좋다.
'Unity & C# > Scripting' 카테고리의 다른 글
(해석)ScriptableObject로 게임을 설계하는 3가지 방법 (0) | 2020.11.17 |
---|---|
Data based structure - ScriptableObject (0) | 2020.11.06 |
C# Extension Method(확장 메서드) (0) | 2020.08.14 |