Effective C# - C#언어 요소
C#으로 개발할 때 주로 사용되는 기능들과 관용적 표현들을 정리
2023.07.15
Effective C# 3판 스터디, 그 첫 번째 포스트
📚 목차
- 🔸 ITEM 1: 지역변수를 사용할 때는
var
를 사용하라- 🔸 ITEM 2:
const
보다는readonly
가 좋다- 🔸 ITEM 3: 캐스트보다는
is
,as
가 좋다.- 🔸 ITEM 4:
string.Format()
을 보간 문자열로 대체하라- 🔸 ITEM 5: 화권 별로 다른 문자열을 생성하려면
FormattableString
을 사용하라- 🔸 ITEM 6:
nameof()
연산자를 적극 활용하라- 🔸 ITEM 7: 델리게이트를 이용하여 콜백을 표현하라
- 🔸 ITEM 8: 이벤트 호출 시에는
null 조건 연산자
를 사용하라- 🔸 ITEM 9: 박싱과 언박싱을 최소화하라
- 🔸 ITEM 10: 베이스 클래스가 업그레이드된 경우에만
new 한정자
를 사용하라
🔸 ITEM 1: 지역변수를 사용할 때는 var
를 사용하라
타입을 명시적으로 드러내지 않는 경우라면 var를 사용하는 것이 좋다.
IEnumerable<string> q =
from c in db.Customers
select c.ContactName;
var q2 = q.Where(s => s.StartsWith(start));
return q2;
q를 var로 받지 않을경우, IQueryable<string>
을 반환해야 하지만
상위객체인 IEnumerable
로 반환하게 되어 Where 구문에서 성능이 저하되게 된다.
var q = from c in db.Customers
select c.ContactName;
var q2 = q.Where(s => s.StartsWith(start));
return q2;
반대로 q를 var로 받았을 경우에는 IQueryable를 반환하게 되어 성능저하가 발생하지 않게 된다.
📌 내장 숫자 타입의 경우,
내장 숫자 타입 (
int
,float
,double
등)을 선언할 때는 명시적으로 타입을 선언하는것이 좋다.예를들어, 다음 코드는
GetMagicNumber()
의 리턴 타입에 따라 total에 저장되는 값이 달라진다.var total = 100 * GetMagicNumber() / 6;
🔸 ITEM 2: const
보다는 readonly
가 좋다
🔹 Const
Const는 컴파일 상수이다.
const로 선언한 변수는, 컴파일 타임에 변수가 값으로 변환된다.
즉, 아래와 같은 코드가 있을 때,
public const int Millennium = 2000;
if (myDateTime.Year == Millennium)
이는 컴파일 시 다음과 같아진다.
if (myDateTime.Year == 2000)
타입을 명시적으로 드러내지 않는 경우라면 var를 사용하는 것이 좋다.
const는 내장된 숫자형, enum, string, null에 대해서만 사용 가능하다.
const의 값 치환 과정은 컴파일에서 적용되므로,
컴파일 할때 사용되는 상숫값을 정의할땐 반드시 const를 사용해야 한다.
readonly보다 성능이 빠르긴 하지만 미미하고, 유연성이 낮다.
🔹 Readonly
Readonly는 런타임 상수이다.
런타임에 값이 평가된다.
생성자에서 초기화 가능, 어떤 타입도 함께 사용할 수 있다.
const는 값이 바뀌면 응용프로그램 전체를 재빌드를 해야하지만,
readonly는 바뀐파일만 배포할 수 있다.
🔸 ITEM 3: 캐스트보다는 is
, as
가 좋다
🔹 Cast
형변환 연산자를 적용할 수 있다.
public class TestType
{
private InnerType _value;
// 사용자 정의 형변환
// TestType을 MyType으로 캐스팅한다.
public static implicit operator MyType(TestType t)
{
return t._value;
}
}
사용자가 상황에 맞게 형변환 연산자를 정의 해줘야한다.
🔹 is,as 연산
사용자 정의 형변환은 수행불가
형변환 과정에서 새로운 객체를 생성하지 않음
value 타입은 as를 사용할 수 없다.
is,as 를 사용하는 것이 가독성이 더 뛰어나다.
// 캐스트 연산
object o = Factory.GetObject();
try
{
MyType t;
t = (MyType)o;
}
catch(InvalidCastException)
{
}
object o = Factiory.GetObject();
MyType t = o as MyType;
if( t != null)
{
}
else
{
}
🔸 ITEM 4: string.Format()
을 보간 문자열로 대체하라
보간 문자열이란 C# 6.0부터 도입된 문자열 포멧팅 방법이다.
var str_1 = "안녕하세요 저는 " + name + " 입니다. 나이는 " + age + "세 입니다.";
var str_2 = string.Format("안녕하세요 저는 {0} 입니다. 나이는 {1}세 입니다.", name, age);
var str_3 = $"안녕하세요 저는 {name}입니다. 나이는 {age}세 입니다.";
🔹 보간 문자열의 장점
- 코드 가독성이 대폭 향상
- 정적 타입 검사를 수행하기 때문에 개발자의 실수를 방지
- 문자열을 생성하기 위한 표현식이 더 풍부함
// 앞에 $를 붙이고 표현식을 {}안에 둔다.
Console.WriteLine($"The value of pi is {Math.PI}");
// 원하는 포매팅을 위한 추가적 인자 전달
Console.WriteLine($"The value of pi is {Math.PI.ToString("F2")}");
// :을 이용해도 가능
Console.WriteLine($"The value of pi is {Math.PI:F2}");
// @를 넣고 ()를 사용해 ':' 가 조건연산자임을 알려줄 수 있다..
Console.WriteLine($@"THe value of pi is {(round ?
Math.PI.ToString() : Math.PI.ToString("F2"))}");
// ?. 연산자와 ?? 연산자도 사용가능!
Console.WriteLine($"The customer's name is {c?.Name ?? "Name is missing"}");
🔸 ITEM 5: 문화권 별로 다른 문자열을 생성하려면 FormattableString
을 사용하라
문자열 보간을 사용하여 문자열을 만들면 반환값이 문자열일수도,
FormattableString
을 상속한 타입일 수도 있다.
FormattableString
을 사용하면 현재 컴퓨터에 지정된 문화권을 고려하여 문자열을 생성할 수 있다.
문화권 코드는 이 블로그를 통해 참고하면 된다.
예를들면 한국어는 ko-KR
, 프랑스어는 fr-FR
이다.
// 프랑스어 문화권으로 변경하는 방법
public static string ToGerman(FormattableString src)
{
return string.Format(
System.Globalization.CultureInfo.CreateSpecificCulture("fr-FR"),
src.Format, src.GetArguments());
}
🔸 ITEM 6: nameof() 연산자를 적극 활용하라
항상 로컬 이름을 문자열로 반환하는 역할을 수행한다.
심볼의 이름을 평가하며, 타입, 변수, 인터페이스, 네임스페이스에 대하여 사용할 수 있다.
심볼의 이름을 바꾸거나 수정할때도 손쉽게 변경 사항을 반영할 수 있다.
예를 들면, 예외 타입에 하드코딩대신 nameof()
를 사용하면 rename을 해도 바로 적용가능하다.
public static void ExceptionMessage(object thisCantBeNull)
{
if(thisCantBeNull == null)
throw new ArgumentNullException(nameof(thisCantBeNull),
"We told you this cant be null!");
}
🔸 ITEM 7: 델리게이트를 이용하여 콜백을 표현하라
델리게이트를 이용하면 타입 안정적인 콜백을 정의할 수 있다.
.NET Framwork 라이브러리는 Predicate<T>
, Action<>
, Func<>
과 같은 형태로
자주 사용되는 델리게이트를 정의해두고 있다.
Multicast가 가능한 콜백을 만들 수 있다.
🔸 ITEM 8: 이벤트 호출 시에는 null 조건 연산자
를 사용하라
C# 6.0에 새롭게 추가된 null 조건 연산자를 고전적인 null check방식 대신 사용하자.
🔹 고전적인 방식
// 구식적인 방법. Updated가 null이 아니여서 if 구문에 들어올때,
// 다른 스레드에서 이벤트 핸들러를 취소할경우 NullReferenceException이 발생.
public void RaiseUpdates()
{
counter++;
if(Updated != null)
Updated(this, counter);
}
🔹 null 조건 연산자
를 이용한 방식
// NEW!: null 조건 연산자를 이용하면 멀티스레딩 환경에도 안전하다.
public void RaiseUpdates()
{
counter++;
Updated?.Invoke(this, counter);
}
🔸 ITEM 9: 박싱과 언박싱을 최소화하라
int i = 123;
object o = i; // 박싱
int j = (int)o; // 언박싱
박싱과 언박싱은 성능에 좋지 않은 영향을 미친다.
수행하는 과정에서 임시객체가 생성되는데, 이로 인해 버그가 발생할 수도 있다.
박싱은 값 타입을 참조 타입으로 변경한다.
🔸 ITEM 10: 베이스 클래스가 업그레이드된 경우에만 new 한정자를 사용하라
베이스 클래스에서 이미 사용하고 있는 메서드를 재정의하여
완전히 새로운 베이스 클래스를 만들어야 하는경우에 new한정자를 사용한다.
public class MyClass
{
public void MagicMethod()
{
}
}
public class MyOhterClass : MyClass
{
// MagicMethod를 재정의
public new void MagicMethod()
{
}
}
🔹 override
와 new
의 차이
virtual method (가상 메서드)는 abstract method (추상 메서드) 와 달리, 자식 클래스에서 재정의 여부는 선택이다.
추상 메서드에서는 재정의는 필수이다.
이때, 가상 메서드가 포함된 클래스를 상속할 경우 자식 클래스에서 재정의 할 때에는
override
, new
두 가지 한정자로 정의할 수 있습니다.
추상 메서드가 포함된 클래스를 상속할때에는 override
만 가능하다.
메서드 | 한정자 |
---|---|
가상 메서드 | override , new |
추상 메서드 | override |
그럼 어떨때 override
와 new
를 사용해야 하는것일까?
이를 정확히 설명하기 위해서는 Up Casting과 Down Casting부터 설명해야 한다.
- Up Casting : 자식 → 부모 형변환. (자동) (Cat 클래스를 Animal 클래스로 변환)
- Down Casting : 부모 → 자식 형변환. (명시적) (Animal 클래스를 Dog 클래스로 변환)
// Up Casting
Animal animal = new Lion();
// Down Casing
Lion lion = (Lion)animal;
1️⃣ Case 1 . override
를 사용 할 경우
class Program {
static void Main(string[] args) {
// Up Casting
Animal animal = new Lion();
animal.Sound();
}
}
public class Animal {
public virtual void Sound() {
Console.WriteLine("부모");
}
}
public class Lion : Animal {
public override void Sound() {
Console.WriteLine("어흥");
}
}
override
는 자신이 상속받은 부모 클래스의 메소드를 재정의 하는 것을 의미한다.Animal.Sound 부모 클래스 메서드
를 숨기고, 자식 클래스인Lion.Sound 메서드
를 재 정의함.- Up Casting 된 animal 객체에서
Sound
를 호출하면, 재정의된Lion.Sound 메서드
가 호출이 되고 콘솔에는어흥
이 찍히게 된다.
2️⃣ Case 2 . new
를 사용 할 경우
class Program {
static void Main(string[] args) {
// Up Casting
Animal animal = new Lion();
animal.Sound();
}
}
public class Animal {
public virtual void Sound() {
Console.WriteLine("부모");
}
}
public class Lion : Animal {
public new void Sound() {
Console.WriteLine("어흥");
}
}
override
와 달리,new
키워드는 부모 클래스의 메서드를 완전히 무시하고 동일한 이름의 새로운 메서드를 만드는 것을 의미한다.Lion.Sound 메서드
는Animal.Sound 메서드
와 완전히 다른 메서드라고 생각하면 된다.- Up Casting 된 animal 객체에서
Sound
를 호출하면, animal 객체의 타입은 Animal 이기 때문에Animal.Sound 메서드
가 호출이 되고 콘솔에는부모
가 찍히게 된다.