Tag Archives: C#

뇌를 자극하는 C# 4.0 프로그래밍/ LINQ

데이터! 데이터! 데이터!

  • LINQ는 C# 언어에 통합된 데이터 질의 기능을 의미한다.

LINQ의 기본: from, where, orderby, select

from

// from 절 사용 방식
from <범위 변수> in <데이터 원본>

// from 절 사용 예시
int[] number = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var result = from n in numbers
             where n % 2 == 0
             orderby n
             select n;
  • 모든 LINQ 쿼리식(Query Expression)은 반드시 from 절로 시작한다. 우리는 쿼리식의 대상이 될 데이터 원본과 데이터 원본 안에 들어있는 각 요소 데이터를 나타내는 범위 변수를 from 절에서 지정해 줘야 한다.
  • 이때 from의 데이터 원본은 아무 형식이나 사용할 수는 없고, IEnumerable<T> 인터페이스를 상속하는 형식이어야만 한다.
    • 배열과 컬렉션은 IEnumerable<T>을 상속하고 있기 때문에 모두 from 절의 데이터 원본으로 사용할 수 있다.
    • foreach의 반복 변수는 데이터 원본으로부터 데이터를 담아내지만, LINQ의 범위 변수는 실제로 데이터를 담지는 않는다. 크래서 쿼리식 외부에서 선언된 변수에 범위 변수의 데이터를 복사해 넣는다거나 하는 일은 할 수 없다. 범위 변수는 오로지 LINQ 질의 안에서만 통용된다.

where

// where 절 사용 예시
Profile[] arrProfile = {
                            new Profile(){ Name = "정우성", Height = 186 },
                            new Profile(){ Name = "김태희", Height = 158 },
                            new Profile(){ Name = "고현정", Height = 172 },
                            new Profile(){ Name = "이문세", Height = 178 },
                            new Profile(){ Name = "하동훈", Height = 171 }
                        }

var porfiles = from profile in arrProfile
               where profile.Height < 175
               select profile;
  • where는 필터 역할을 하는 연산자이다. from절이 데이터 원본으로부터 뽑아낸 범위 변수가 가져야 하는 조건을 where 연산자에게 매개 변수로 입력하면 LINQ는 해당 조건에 부합하는 데이터만 걸러낸다.

orderby

// orderby는 기본적으로 데이터를 오름차순으로 정렬한다.
var porfiles = from profile in arrProfile
               where profile.Height < 175
               orderby profile.Height
               select profile;

// ascending은 오름차순을 명시적으로 기술한다
var porfiles = from profile in arrProfile
               where profile.Height < 175
               orderby profile.Height ascending
               select profile;

// 내림차순은 descending을 기술하면 된다.
var porfiles = from profile in arrProfile
               where profile.Height < 175
               orderby profile.Height descending
               select profile;

select

// select는 최종 결과를 추출해 내는 키워드이다.
// 추출된 데이터는 IEnumerable<T> 형태로 반환된다.
// 아래의 경우 IEnumerable<Profile>이 반환된다.
var porfiles = from profile in arrProfile
               where profile.Height < 175
               orderby profile.Height
               select profile;

// 프로퍼티만 추출할 수도 있다.
// 이 프로퍼티는 IEnumerable<string> 형식으로 반환된다.
var porfiles = from profile in arrProfile
               where profile.Height < 175
               orderby profile.Height
               select profile.Name;

// select 문은 무명 형식을 이용해서 새로운 형식을 즉석에서 만들어 낼 수도 있다.
var porfiles = from profile in arrProfile
               where profile.Height < 175
               orderby profile.Height
               select new { Name = profile.Name, InchHeight = profile.Height * 0.393 };

여러 개의 데이터 원본에 질의하기

// 여러 개의 데이터 원본에 접근하는 예시
class Class
{
    public string Name { get; set; }
    public int[] Score { get; set; }
}

Class[] arrClass = 
{
    new Class(){ Name = "연두반", Score = new int[]{ 99, 80, 70, 24 }},
    new Class(){ Name = "분홍반", Score = new int[]{ 60, 45, 87, 72 }},
    new Class(){ Name = "파랑반", Score = new int[]{ 92, 30, 85, 94 }},
    new Class(){ Name = "노랑반", Score = new int[]{ 90, 88, 0, 17 }},
}

// 여러 개의 데이터에 접근하고 싶다면 for문 처럼 from을 중첩해서 사용하면 된다.
var classes = from c in arrClass // 첫 번째 데이터 원본
              from s in c.Score // 두 번째 데이터 원본
              where s < 60
              select new { c.Name, Lowest = s };

group by로 데이터 분류하기

// group by 선언 방법
group (from에서 뽑아낸 범위 변수) by (분류 기준) into (그룹 변수)

// group by 사용 예시
Profile[] arrProfile = {
                            new Profile(){ Name = "정우성", Height = 186 },
                            new Profile(){ Name = "김태희", Height = 158 },
                            new Profile(){ Name = "고현정", Height = 172 },
                            new Profile(){ Name = "이문세", Height = 178 },
                            new Profile(){ Name = "하동훈", Height = 171 }
                        }

var listProfile = from profile in arrProfile
                  group profile by profile.Height < 175 into g
                  select new { GroupKey = g.key, Profiles = g };

두 데이터 원본을 연결하는 join

내부 조인

// 내부 조인 선언 방법
from a in A
join b in b on a.xxxx equals b.yyyy

// 내부 조인 사용 예시
class Profile
{
    public string Name { get; set; }
    public int Height { get; set; }
}

class Product
{
    public string Title { get; set; }
    public string Star { get; set; }
}

var listProfile = from profile in arrProfile
                  join product in arrProduct on profile.Name equals product.Star
                  select new ( Name = profile.Name, Work = product.Title, Height = profile.Height );
  • 내부 조인(Inner Join)은 교집합과 비슷하다. 두 데이터 원본 사이에서 일치하는 데이터들만 연결한 후 반환하기 때문.
  • 내부 조인은 첫 번째(왼쪽) 데이터 원본의 데이터를 기준으로 이 데이터의 특정 필드와 두 번째(오른쪽) 데이터 원본이 갖고 있는 각 데이터의 특징 필드를 비교해서 일치하는 데이터들만 모아 반환한다.

외부 조인

// 외부 조인 사용 예시
class Profile
{
    public string Name { get; set; }
    public int Height { get; set; }
}

class Product
{
    public string Title { get; set; }
    public string Star { get; set; }
}

// 외부 조인시에는 조인을 수행한 후 그 결과를 임시 컬렉션에 저장하고, 이 임시 컬렉션에 대해 DefaultIfEmpty 연산을 수행해서 비어 있는 조인 결과에 빈 값을 채워 넣는다.
// DefaultIfEmpty 연산을 거친 임시 컬렉션에서 from 절을 통해 범위 변수를 뽑아내고 이 범위 변수와 기준 데이터 원본에서 뽑아낸 범위 변수를 이용해서 결과를 추출해 낸다.
var listProfile = from profile in arrProfile
                  join product in arrProduct on profile.Name equals product.Star into ps
                  from product in ps.DefaultIfEmpty(new Product(){Title="없음"})
                  select new ( Name = profile.Name, Work = product.Title, Height = profile.Height );
  • 외부 조인은 합집합과 비슷하다.
  • 외부 조인시 연결할 데이터 원본에 기준 데이터 원본의 데이터와 일치하는 데이터가 없다면 그 부분은 빈 값으로 결과를 채운다.
  • 참고) 원래 SQL에서 지원하는 외부 조인은 왼쪽 조인, 오른쪽 조인, 완전 조인이 있는데 LINQ는 왼쪽 조인만 지원한다.

LINQ의 비밀, 그리고 LINQ 표준 연산자

// 프로그래머가 작성한 LINQ 쿼리식
var profiles = from profile in arrProfile
               where profile.Height < 175
               orderby profile.Height
               select new { Name = profile.Name, InchHeight = profile.Height * 0.393 };

// C# 컴파일러는 위 쿼리식을 분석하여 다음과 같은 호출 코드로 번역한다.
// 다시 말해 아래와 같은 메소드 형식으로 작성해도 된다.
var profiles = arrProfile
                .Where( profile => profile.Height < 175 )
                .OrderBy( profile => profile.Height)
                .Select( profile => new { Name = profile.Name, InchHeight = profile.Height * 0.393 } );
  • LINQ는 .NET 언어 중에서도 C#과 비주얼베이직에서만 사용 가능하다. MS는 LINQ 쿼리식이 실행될 수 있도록 CLR을 개선하는 대신 C# 컴파일러와 VB 컴파일러를 업그레이드 했기 때문.
  • 프로그래머가 LINQ 쿼리식을 작성하면 C# 컴파일러는 위와 같은 쿼리를 분석해서 메소드 호출 코드로 만들어 낸다.

LINQ 표준 연산자

종류 메소드 이름 설명
정렬 OrderBy 오름차순으로 값을 정렬
정렬 OrderByDescending 내림차순으로 값을 정렬
정렬 ThenBy 오름차순으로 2차 정렬
정렬 ThenByDescending 내림차순으로 2차 정렬
정렬 Reverse 컬렉션의 요소를 거꾸로 뒤집는다.
집합 Distinct 중복 값을 제거
집합 Except 두 컬렉션 사이의 차집합을 반환
집합 Intersect 두 컬렉션 사이의 교집합을 반환
집합 Union 두 컬렉션 사이의 합집합을 반환
필터링 OfType 메소드의 형식 매개 변수로 형식 변환이 가능한 값들만 추출
필터링 Where 필터링할 조건을 평가하는 함수를 통과하는 값들만 추출
수량 연산 All 모든 요소가 임의의 조건을 모두 만족 시키는지를 평가. 결과는 true 또는 false
수량 연산 Any 모든 요소 중 단 하나의 요소라도 임의의 조건을 만족시키는지를 평가. 결과는 true 또는 false
수량 연산 Contains 명시한 요소가 포함되어 있는지를 평가. 결과는 true 또는 false
데이터 추출 Select 값을 추출하여 시퀀스를 만든다.
데이터 추출 SelectMany 여러 개의 데이터 원본으로부터 값을 추출하여 하나의 시퀀스를 만든다. 여러 개의 from절을 사용한다.
데이터 분할 Skip 시퀀스에서 지정한 위치까지 요소들을 건너 뛴다.
데이터 분할 SkipWhile 입력된 조건 함수를 만족시키는 요소들을 건너 뛴다.
데이터 분할 Take 시퀀스에서 지정한 요소까지 요소들을 취한다.
데이터 분할 TakeWhile 입력된 조건 함수를 만족시키는 요소들을 취한다.
데이터 결합 Join 공통 특성을 가진 서로 다른 두 개의 데이터 소스의 객체를 연결한다. 공통 특성을 키(Key)로 삼아, 키가 일치하는 두 객체를 쌍으로 추출한다.
데이터 결합 GroupJoin 기본적으로 Join 연산자와 같은 일을 하되, 조인 결과를 그룹으로 만들어 넣는다.
데이터 그룹화 GroupBy 공통된 특성을 공유하는 요소들을 각 그룹으로 묶는다. 각 그룹은 IGrouping 객체로 표현된다.
데이터 그룹화 ToLookup 키(key) 선택 함수를 이용하여 골라낸 요소들을 Lookup 형식의 객체에 삽입한다.
생성 DefaultIfEmpty 빈 컬렉션을 기본값이 할당된 싱글턴 컬렉션으로 바꾼다.
생성 Empty 비어 있는 컬렉션을 반환한다.
생성 Range 일정 범위의 숫자 시퀀스를 담고 있는 컬렉션을 생성한다.
생성 Repeat 같은 값이 반복되는 컬렉션을 생성한다.
동등 여부 평가 SequenceEqual 두 시퀀스가 서로 일치하는지를 평가한다.
요소 접근 ElementAt 컬렉션으로부터 임의의 인덱스에 존재한느 요소를 반환한다.
요소 접근 ElementAtOrDefault 컬렉션으로부터 임의의 인덱스에 존재하는 요소를 반환하되, 인덱스가 컬렉션의 범위를 벗어날 때 기본값을 반환한다.
요소 접근 First 컬렉션의 첫 번째 요소를 반환. 조건식이 있으면 조건을 만족 시키는 첫 번째 요소를 반환
요소 접근 FirstOrDefault First 연산자와 같은 기능을 하되, 반환할 값이 없는 경우 기본 값을 반환
요소 접근 Last 컬렉션의 마지막 요소를 반환. 조건식이 있으면 조건을 만족 시키는 마지막 요소를 반환
요소 접근 LastOrDefault Last 연산자와 같은 기능을 하되, 반환할 값이 없는 경우 기본값을 반환
요소 접근 Single 컬렉션의 유일한 요소를 반환. 조건식이 있으면 조건을 만족시키는 유일한 요소를 반환
요소 접근 SingleOrDefault Single 연산자와 같은 기능을 하되, 반환할 값이 없는 경우 기본값을 반환
형식 변환 AsEnumerable 매개 변수를 IEnumerable로 형식 변환하여 반환
형식 변환 AsQueryable (일반화) IEnumerable 객체를 (일반화) IQueryable 형식으로 반환
형식 변환 Cast 컬렉션의 요소들을 특정 형식으로 반환
형식 변환 OfType 특정 형식으로 형식 변환할 수 있는 값만 걸러 낸다.
형식 변환 ToArray 컬렉션을 배열로 변환. 이 메소드는 강제로 쿼리를 실행한다.
형식 변환 ToDictionary 키 선택 함수에 근거해서 컬렉션의 요소를 Dictionary에 삽입. 이 메소드는 강제로 쿼리를 실행한다.
형식 변환 ToList 컬렉션을 List로 형식 변환한다. 이 메소드는 강제로 쿼리를 실행한다.
형식 변환 ToLookup 키 선택 함수에 근거해서 컬렉션의 요소를 Lookup에 삽입. 이 메소드는 강제로 쿼리를 실행한다.
연결 Concat 두 시퀀스를 하나의 시퀀스로 연결한다.
집계 Aggregate 컬렉션의 각 값에 대해 사용자가 정의한 집계 연산을 수행한다.
집계 Average 컬렉션의 각 값에 대한 평균을 계산한다.
집계 Count 컬렉션에서 조건에 부합하는 요소의 갯수를 센다.
집계 LongCount Count와 동일한 기능을 하지만 매우 큰 컬렉션을 대상으로 한다는 점이 다르다.
집계 Max 컬렉션에서 가장 큰 값을 반환한다.
집계 Min 컬렉션에서 가장 작은 값을 반환한다.
집계 Sum 컬렉션 내의 값의 합을 계산한다.
// LINQ 메소드 사용 예시
Profile[] arrProfile = {
                            new Profile(){ Name = "정우성", Height = 186 },
                            new Profile(){ Name = "김태희", Height = 158 },
                            new Profile(){ Name = "고현정", Height = 172 },
                            new Profile(){ Name = "이문세", Height = 178 },
                            new Profile(){ Name = "하동훈", Height = 171 }
                        }

// 쿼리를 날려 데이터를 담은 후에 LINQ 메소드로 결과 출력                        
var profiles = from profile in arrProfile
               where profile.Height < 180
               select profile;

double Average1 = profiles.Average(profile => profile.Height);
Console.WriteLine(Average1); // 169.75 출력

// 쿼리식과 LINQ 메소드를 한 번에 사용
double Average2 = (from profile in arrProfile
                   where profile.Height < 180
                   select profile).Average(profile => profile.Height);
Console.WriteLine(Average2); // 169.75 출력

뇌를 자극하는 C# 4.0 프로그래밍/ 람다식

람다식, 너는 어디에서 왔니?

  • 람다식 (Lambda Expression)이란 1936년 수학자 알론조 처치 (Alonzo Church)가 발표한 람다 계산법(Lambda Calculus)에서 사용하는 식.
  • 알론조 처치의 제자인 존 메카시 (John McCarthy)가 이것을 프로그래밍 언어에 도입할 수 있겠다는 아이디어를 냈고 50년대 말에 LISP 이라는 언어를 만듦.

처음으로 만들어 보는 람다식

// 람다식 선언 방법
매개변수목록 => 식

// 람다식 사용 예시
delegate int Calculate (int a, int b);

static void Main(string[] args)
{
    // 두 개의 int 형식 매개 변수 a, b를 받아 이 둘을 더해 반환하는 익명 메소드를 람다식으로 선언.
    Calculate calc1 = (int a, int b) => a + b;

    // C# 컴파일러는 형식 유추(Type Inference)라는 기능이 있기 때문에 아래와 같이 선언해도 된다.
    Calculate calc2 = (a, b) => a + b;

    // 델리게이트 익명 메소드를 이용하면 아래와 같이 기술된다.
    Calculate calc3 = delegate(int a, int b)
                      {
                          return a + b;
                      }
}
  • => 연산자는 ‘입력’ 연산자이다. 이 연산자가 하는 역할은 그저 매개변수를 전달하는 것 뿐이다.
  • C#에 델리게이트 익명 메소드와 람다식이 공존하는 이유는 람다식이 더 늦게 도입됐기 때문. 델리게이트를 이용한 익명 메소드는 C# 2.0 버전에서 도입되었고 람다식은 C# 3.0 버전에 도입되었다.

문 형식의 람다식

// 문 형식의 람다식 선언 방법
(매개변수목록) => {
                      문장 1;
                      문장 2;
                      문장 3;
                  }

// 문 형식의 람다식 사용 예시
delegate void DoSomething ();

static void Main(string[] args)
{
    // 매개 변수가 없으면 ()에 아무것도 넣지 않는다.
    DoSomething DoIt = ( ) => {
                                  Console.WriteLine("1");
                                  Console.WriteLine("2");
                                  Console.WriteLine("3");
                              }
}

Func와 Action으로 더 간편하게 무명 함수 만들기

  • 익명 메소드나 무명 함수를 만들기 위해선 매번 별개의 델리게이트를 선언해야 하는데, 이게 꽤나 번거롭다. 이 번거로움을 해결하기 위해 .NET Framework에는 Func 델리게이트와 Action 델리게이트가 미리 선언되어 있다.
  • Func 델리게이트는 결과를 반환하는 메소드를 참조하고, Action 델리게이트는 결과를 반환하지 않는 메소드를 참조한다.

Func 델리게이트

// Func 델리게이트 사용 예시
Func<int> func1 = () => 10; // 입력 매개변수는 없으며 반환은 int
Func<int, int> func2 = (x) => x * 2; // 입력 매개변수는 int, 출력 매개 변수도 int
Func<int, int, int> func3 = (x, y) => x + y; // 입력 매개변수는 int, int 출력 매개 변수도 int
  • .NET Framework에는 17가지 버전의 Func 델리게이트가 준비되어 있다.
  • Func 델리게이트는 가장 마지막에 있는 매개 변수가 반환 형식이 된다. Func 델리게이트의 매개변수의 수는 ‘입력 매개 변수 수 + 출력용 매개 변수 1개’가 된다.

Action 델리게이트

// Action 델리게이트 사용 예시
Action act1 = () => Console.WriteLine("Action()");
act1();

int result = 0;
Action<int> act2 = (x) => result = x * x; // 람다식 밖에 선언된 result에 x * x가 대입 된다.
act2(3);
Console.WriteLine("result: {0}", result); // 9가 출력 됨.

Action<double, double> act3 = (x, y) => {
            double pi = x / y;
            Console.WriteLine("Action<T1, T2>({0}, {1}): {2}", x, y, pi);
        }
act3(22.0, 7.0);

식 트리

  • 트리는 위 그림과 같이 노드(Node: 마디)로 구성되며, 각 노드는 서로 부모-자식 관계로 연결된다. 위 그림에서 A의 자식 노드는 B, C가 되고 B의 자식 노드는 D, E가 된다.
  • 한편 최상위 노드인 A를 루트(Root) 노드라고 하며 가장 끝에 있는 D, E, F, G와 같은 노드를 잎(Leaf) 노드 또는 단말(Terminal) 노드라고 한다.
  • 트리 자료 구조는 부모 노드가 여러 개의 자식 노드를 가질 수도 있지만, 식 트리는 한 부모 노드가 단 두 개만의 자식 노드를 가질 수 있는 이진 트리(Binary Tree)이다.

  • 식 트리란 식을 트리로 표현한 자료 구조를 말한다. 예컨대 1*2+(7-8)을 식 트리로 표현하면 위 그림과 같아진다.
  • 식 트리에서 연산자는 부모 노드가 되며, 피연산자는 자식 노드가 된다.
    • 위 그림에서 1*2에서 *는 부모 노드, 1과 2는 *의 자식 노드가 딘다.
    • 이런 식으로 트리의 잎 노드부터 계산해서 루트 노드까지 올라가면 전체 식의 결과를 얻을 수 있다.
  • 식 트리 자료 구조는 컴파일러나 인터프리터를 제작하는데 응용된다. 컴파일러는 프로그래밍 언어의 문법을 따라 작성된 소스 코드를 분석해서 식 트리로 만든 후 이를 바탕으로 실행 파일을 만든다.
  • 완전한 C# 컴파일러는 아니지만, C#은 프로그래머가 C# 코드 안에서 직접 식 트리를 조립하고 컴파일해서 사용할 수 있는 기능을 제공한다. 다시 말해 런타임 중에 동적으로 무명 함수를 만들어 사용할 수 있게 해준다는 뜻이다.
  • 식 트리를 다루는데 필요한 클래스들은 .NET Framework의 System.Linq.Expressions 네임스페이스 안에 준비되어 있다.
Expression의 파생 클래스 설명
BinaryExpression 이항 연산자(+에서부터 <=까지)를 갖는 식을 표현한다.
BlockExpression 변수를 정의할 수 있는 식을 갖는 블록을 표현한다.
ConditionalExpression 조건 연산자가 있는 식을 나타낸다.
ConstantExpression 상수가 있는 식을 나타낸다.
DefaultExpression 형식(type)이나 비어 있는 식의 기본값을 표현한다.
DynamicExpression 동적 작업을 나타낸다.
GotoExpression return, break, continue, goto와 같은 점프문을 나타낸다.
IndexExpression 배열의 인덱스 참조를 나타낸다.
InvocationExpression 델리게이트나 람다식 호출을 나타낸다.
LabelExpression 레이블을 나타낸다.
LambdaExpression 람다식을 나타낸다.
ListInitExpression 컬렉션 이니셜라이저가 있는 생성자 호출을 나타낸다.
LoopExpression 무한 반복을 나타낸다.
MemberExpression 객체의 필드나 속성을 나타낸다.
MemberInitExpression 생성자를 호출하고 새 객체의 멤버를 초기화하는 동작을 나타낸다.
MethodCallExpression 메소드 호출을 나타낸다.
NewArrayExpression 새 배열의 생성과 초기화를 나타낸다.
NewExpression 생성자 호출을 나타낸다.
ParameterExpression 명명된 매개 변수를 나타낸다.
RuntimeVariablesExpression 변수에 댛나 런타임 읽기/쓰기 권한을 제공한다.
SwitchExpression 다중 선택 제어 식을 나타낸다.
TryExpression try~catch~finally 블록을 나타낸다.
TypeBinaryExpression 형식 테스트를 비롯한 형식(type)과 식(expression)의 연산을 나타낸다.
UnaryExpression 단항 연산자를 갖는 식을 나타낸다.

 

  • 위 표의 클래스들은 Expression 클래스의 파생 클래스들이다. Expression 클래스는 식 트리를 구성하는 노드를 표현한다. 따라서 Expression을 상속 받는 위 클래스들이 식 트리의 각 노드를 표현할 수 있게 된다.

식 트리 예제

/* 
 * 식 트리 만들기
*/

// 팩토리 메서드 패턴
Expression const1 = Expression.Constant(1); // 상수 1
Expression param1 = Expression.Parameter(typeof(int), "x"); // 매개 변수 x

Expression exp = Expression.Add(const1, param1); // 1 + x

// 여기까지는 식을 트리로 표현한 것에 불과하다.
// exp는 실행 가능한 상태가 아니고 그저 '데이터' 상태에 머물러 있다는 것
// exp가 자신의 트리 자료 구조 안에 정의되어 있는 식을 실행할 수 있으려면 람다식으로 컴파일 되어야 한다.
// 람다식으로의 컴파일은 Expression<TDelegate> 클래스를 이용한다.

Expression<Func<int, int>> lamda1 = Expresison<Func<int, int>>.Lambda<Func<int, int>>( exp, new ParameterExpression[]{ (ParameterExpression)param1 } );

Func<int, int> compiledExp = lambda1.Compile(); // 실행 가능한 코드로 컴파일

Console.WriteLine(compileExp(3)); // 코드가 1+x 이므로 4가 출력 됨.


/*
 * 람다식으로 식 트리 만들기
*/
// 2개의 인자를 받아서 1개를 출력하는 무명 함수
Expression<Func<int, int, int>> expression = (a, b) => 1 * 2 + (a - b);

// 위의 람다식으로 쓰여진 식을 컴파일한다.
Func<int, int, int> func = expresion.Compile();

Console.WriteLine("1 * 2 + ({0}-{1}) = {2}", 7, 8, func(7, 8));
  • 람다식을 이용하면 간편하게 식 트리를 만들 수 있지만 ‘동적으로’ 식 트리를 만들기는 어렵다. Expression 형식은 불변(Immutable)이기 때문에 한 번 인스턴스가 만들어지고 난 후에는 변경할 수가 없기 때문.
  • 식 트리는 코드를 “데이터”로서 보관할 수 있다. 이것은 파일에 저장할 수도 있고 네트워크를 통해 다른 프로세스에 전달할 수도 있다. 심지어 코드를 담고 있는 트리 데이터를 데이터베이스 서버에 보내서 실행시킬 수도 있다.

뇌를 자극하는 C# 4.0 프로그래밍/ 델리게이트와 이벤트

델리게이트란?

// 델리게이트 선언 방법
한정자 delegate 반환형식 델리게이트이름 (매개변수목록);

// 델리게이트 사용 예시
int Plus(int a, int b)
{
    return a + b;
}

int Minus(int a, int b)
{
    return a - b;
}

delegate int MyDelegate(int a, int b);
MyDelegate Callback;

Callback = new MyDelegate(Plus);
Console.WriteLine(Callback(3, 4));

Callback = new MyDelegate(Minus);
Console.WriteLine(Callback(7, 5));
  • C#에서 델리게이트는 콜백(Callback)을 구현하기 위해 사용된다.
  • 델리게이트는 메서드에 대한 참조이다.
  • 델리게이트에 메서드의 주소를 할당한 후 델리게이트를 호출하면 이 델리게이트가 메서드를 호출해 준다.
    • 델리게이트는 여러 메서드를 참조할 수 있기 때문에 –델리게이트 체인– 옵저버 패턴을 쉽게 구성할 수 있게 해준다.

델리게이트는 왜, 그리고 언제 사용하나요?

  • 값이 아닌 코드 자체를 매개변수로 넘기고 싶을 때 델리게이트를 사용한다.
    • 여러 곳에서 하나의 동일한 코드를 실행하는데, 코드 완료 후 각기 다른 메서드를 실행해야 하는 경우, 코드를 받는 입장에서 코드 로직은 같지만 자신의 코드를 완료한 후 실행해야 하는 메서드가 달라져 여러 메서드를 따로 만들어야 하는 중복 코드가 발생하게 된다.
    • 이 때 코드 완료 후에 실행해야 하는 각기 다른 메서드 자체를 매개변수로 넘겨주면 코드를 받는 입장에서는 자신의 코드를 완료한 후에 매개변수로 넘겨 받은 메서드를 실행해 버리면 되기 때문에 코드가 매우 간편해 진다.
    • 이런 역할을 하는게 바로 델리게이트이다. 매개변수로 메서드를 넘길 수 있게 해줘서 코드 중복을 줄이는 것.
    • 더불어 델리게이트는 델리게이트 체인을 이용하여 델리게이트에 등록된 여러 메서드들을 한번에 실행하게 하는 강력한 기능을 갖고 있는데, 이는 이벤트라는 기능으로 이어진다.
  • (매개변수를 넘기는 예시 생략)

일반화 델리게이트

// 일반화 델리게이트 사용 예시
delegate int Compare<T> (T a, T b);

static int AscendCompare<T> (T a, T b)
{
    return a.CompareTo(b);
}
  • 일반화 메서드와 동일하게 델리게이트도 일반화 할 수 있다.

델리게이트 체인

// 델리게이트 체인 사용 예시
void Call119 (string location)
{
    Console.WriteLine("소방서죠? 불났어요! 주소는 {0}", location);
}
void ShotOut (string location)
{
    Console.WriteLine("피하세요! {0}에 불이 났어요!", location);
}
void Escape (string location)
{
    Console.WriteLine("{0}에서 나갑시다!", location);
}

delegate void ThereIsAFire(string location);

// +를 이용해서 하나의 델리게이트에 여러 메서드를 넣을 수 있다.
ThereIsAFire fire = new ThereIsAFire(Call119);
fire += new ThereIsAFire(ShotOut);
fire += new ThereIsAFire(Escape);
  • 델리게이트는 하나가 여러 개의 메서드를 동시에 참조할 수 있다는 특징이 있다.
  • 하나의 델리게이트에 여러 메서드를 넣으려면 + 기호를 이용하면 된다.
  • 만일 델리게이트에 넣어진 메서드 중 하나를 제거해야 한다면 - 기호를 이용하면 된다.

익명 메서드

// 익명 메서드의 선언 방법
델리게이트 인스턴스 = delegate(매개변수목록)
                        {
                            // 실행 코드
                        }

// 익명 메서드 사용 예시
public static void Main()
{
    Calculate Calc;

    Calc = delegate(int a, int b)
            {
                return a + b;
            }

    Console.WriteLine("3 + 4: {0}", Calc(3, 4));
}
  • 델리게이트가 참조할 메서드를 넘겨야 할 일이 생겼는데 이 메서드를 두 번 사용할 일이 없다고 판단되면 위와 같이 메서드를 익명으로 만들어 사용하면 된다.

이벤트: 객체에 일어난 사건 알리기

// 이벤트가 발생했을 경우 그것을 구독자에게 알려줄 발행자 클래스.
// 발행자는 구독자가 누구인지 모르고 다만 구독자들에게 이벤트가 발생했을 시 그것을 알리는 역할만 한다.
class Publisher
{
    //이벤트를 위한 델리게이트 정의. 델리게이트 자체는 밖에 있어도 무방하다.
    public delegate void MyEventHandler();

    // 델리게이트에 event 한정자를 붙여서 이벤트를 정의한다.
    // 델리게이트 체인 기능을 이용하여 이벤트가 발생했을시 구독하는 객체들의 메서드를 모두 실행한다.
    public event MyEventHandler Click;

    // 이벤트 발생시 실행되는 메서드
    public void DoClick()
    {
        if(Click != null) // 이 객체의 이벤트를 구독하는 구독자가 있는지 검사
        {
            // 이벤트 발생. 구독자들에게 이벤트를 통지함.(델리게이트 호출)
            Click(); 
        }
    }
}

// 이벤트를 구독하는 구독자(Subscriber) 클래스 
// 이벤트가 발생했을시 그것을 들을지 말지는 각 클래스에서 스스로 정한다.
class Subscriber
{       
    static void Main()
    {
        // 이벤트 발행자 객체 생성
        Publisher p = new Publisher();

        // 이벤트를 구독한다. 
        // 이벤트가 발생했을시 실행할 자신의 메서드를 등록해 둔다.
        p.Click += new Publisher.MyEventHandler(p_Click);
    }

    // Publisher 객체에서 Click 이벤트가 발생할 때 호출되는 메서드
    // 델리게이트 호출을 통해 이 메서드를 이벤트에 등록해 둔다.
    // 이벤트 구독자는 여럿일 것이고, 발행자는 이벤트가 발생했을시 델리게이트 체인을 이용하여 각 구독자가 등록해 둔 메서드들을 모두 실행하게 된다.
    static void p_Click()
    {
        Console.WriteLine("Publisher 객체의 Click 이벤트가 발생하였습니다");
    }               
}
  • (책에 나온 설명보다 좀 더 잘 정리된 내용으로 정리. 이벤트는 publisher-subscriber 관계로 이해하는게 가장 명확하다. 발행자-구독자 관계로 보면 C#의 이벤트는 옵저버 패턴을 담고 있다고 할 수 있다.)
  • C#에서 이벤트는 델리게이트를 event 한정자로 수식해서 만든다. 이벤트가 델리게이트 형식인 이유는 이벤트가 발생했을 시 델리게이트 체인을 이용하여 이벤트 구독자들에게 한 번에 알리기 위함. 이벤트를 갖고 있는 클래스가 발행자가 된다.
  • 이벤트 구독자들은 이벤트가 발생했을 시 실행할 메서드를 이벤트에 등록해 두어서 이벤트가 실행되면 메서드가 자동으로 실행되게 한다.
  • 이벤트는 외부에서 실행시킬 수 없으며, 오로지 발행자만이 이벤트를 실행할 수 있다. 이벤트 구독자들은 이벤트가 발생했을시 실행할 메서드를 등록할 수만 있다. 이는 보안 관련 이슈인데 자세한 내용은 다음 섹션에서 설명.

델리게이트와 이벤트

  • 이벤트와 델리게이트의 가장 큰 차이점은 이벤트는 외부에서 직접 사용할 수 없다는 것. 반면 델리게이트는 외부에서 얼마든지 호출할 수 있다.
  • 이벤트가 외부에서 호출될 수 없다는 사실은 견고한 이벤트 기반 프로그래밍을 가능하게 한다. 이벤트는 발행자만 발생시킬 수 있기 때문에 –누구나 들을 수는 있지만– 신뢰할 수 있다.

뇌를 자극하는 C# 4.0 프로그래밍/ 예외 처리하기

예외에 대하여

  • 예외가 발생했을 때 코드 내에 이를 처리하는 구문이 없다면 최종적으로 CLR에게 예외가 던져지게 된다. CLR은 자신에게 던져진 예외를 “처리되지 않은 예외”로 보고 이것을 사용자에게 출력한 뒤 프로그램을 강제로 종료시켜 버린다.
  • 그러므로 프로그래머는 예외가 자신이 작성한 코드 내에서 처리 되도록 조치를 취해야 한다.

try~catch로 예외 받기

// try~catch를 이용하여 예외를 처리하는 방법
try
{
    // 실행 코드
    // 예외가 발생할 가능성이 있는 코드를 try 감싼다.
}
catch( 예외객체1 )
{
    // 예외가 발생했을 때의 처리
}
catch( 예외객체2 )
{
    // 예외가 발생했을 때의 처리
}
  • try 안에 있는 코드에서 예외가 발생하면 catch가 받아내여 예외를 처리한다.
  • 이때 catch 절은 try 블록에서 던질 예외 객체와 형식이 일치해야 하는데, 그렇지 않으면 던져진 예외를 아무도 받지 못해서 “처리되지 않은 예외”로 남게 된다.
  • 만일 try 블록에서 발생하는 예외가 여러 종류라면 catch 블록도 여러개를 둘 수 있다.

System.Exception 클래스

  • System.Exception 클래스는 모든 예외의 조상이기 때문에 System.Exception을 받는 예외절 하나면 모든 예외를 다 받아낼 수 있다.
  • 다만 System.Exception 형식은 프로그래머가 발생할 것으로 계산한 예외 이상을 받아내기 때문에 상위코드에서 처리해야 할 예외를 받아낼 수도 있으니 주의해서 사용해야 한다.

예외 던지기

// throw로 예외 던지는 방법
try
{
    throw new Exception("예외를 던집니다");
}
catch(Exception e)
{
    Console.WriteLine(e.Message);
}

// throw로 예외 던지는 예시
static void DoSomething(int arg)
{
    if ( arg < 10 )
    {
        Console.WriteLine("arg: {0}", arg);
    }
    else
    {
        throw new Exception("arg가 10보다 큽니다");
    }
}

static void Main()
{
    try
    {
        DoSomething(12);
    }
    catch(Exception e)
    {
        Console.WriteLine(e.Message);
    }
}
  • 예외를 던지는 throw 구문이 있는 블록을 try로 감싼다고 생각하면 된다. 거기서 나오는 예외를 catch에서 받아서 처리하게 됨.

try~catch와 finally

// finally 사용 예시
try
{
    db.Open(); // 데이터베이스 커넥션
}
catch (Exception e)
{
    // 예외처리
}
finally
{
    db.Close(); // 예외가 발생하여 미처 해제하지 못한 자원은 finally에서 하면 안전하다.
}
  • try 블록에서 예외가 던져지면 프로그램은 catch 절로 점프하게 되는데, 만약 예외처리 때문에 try 블록에서 할당한 자원을 해제를 하지 않은 경우 버그가 원인이 된다.
  • finally는 자신이 붙어 있는 try절이 실행되면 반드시 실행되는 절이기 때문에 자원 해제와 같은 뒷마무리 코드는 finally 절을 이용하면 된다.
    • 예외를 throw한 경우 외에도 예외가 발생하지 않고 정상적으로 마무리 되더라도 finally는 실행된다.

사용자 정의 예외 클래스 만들기

  • Exception 클래스를 상속하기만 하면 사용자 정의 예외 클래스를 만들 수 있다.
  • (실제 클래스 만드는 내용 생략)

예외 처리 다시 생각해 보기

  • 예외처리의 장점
    1. try-catch 문을 이용한 예외 처리는 실제 일을 하는 코드와 문제를 처리하는 코드를 분리함으로써 코드를 간결하게 만들어 준다. 코드 작성과 가독성이 모두 좋다.
    2. 예외 객체의 StackTrace 프로퍼티를 통해 문제가 발생한 부분의 소스 코드 위치를 알려 주기 때문에 디버깅이 용이하다.
    3. 여러 문제점을 하나로 묶어내거나 코드에서 발생할 수 있는 오류를 종류별로 정리해 주는 효과가 있다. 예컨대 DivideZeroException 예외를 일으킬 수 있는 부분이 둘 이상일 수 있는데 이 형식의 예외를 받는 catch 블록 하나면 모두 처리할 수 있다.

뇌를 자극하는 C# 4.0 프로그래밍/ 일반화 프로그래밍

일반화 프로그래밍이란?

  • 로직은 같지만 데이터 형식만 다른 메서드나 클래스를 일반화 하여 하나로 만드는 것. 중복 코드를 줄인다.

일반화 메소드

// 일반화 메소드 선언 방법
한정자 반환형식 메소드이름 <형식매개변수> (매개변수목록)
{
    // 내부 구현
}

// 일반화 메소드 사용 예시
void CopyArray<T> ( T[] source, T[] target )
{
    for ( int i = 0 ; i < source.Length ; i++ )
    {
        target[i] = source[i];
    }
}

int[] source = { 1, 2, 3, 4, 5 };
int[] target = new int[source.Length];

CopyArray<int>(source, target);

string[] source2 = { "하나", "둘", "셋", "넷", "다섯" };
string[] target2 = new string[source2.Length];

CopyArray<string>(source2, target2);

일반화 클래스

// 일반화 클래스 선언 방법
class 클래스이름 <형식매개변수>
{
    // 내부 구현
}

// 일반화 클래스 사용 예시
class Array_Generic<T>
{
    private T[] array;
    public T GetElement(int index)
    {
        return array[index];
    }
}

Array_Generic<int> intArr = new Array_Generic<int>();
Array_Generic<double> doubleArr = new Array_Generic<double>();

형식 매개 변수 제약시키기

// 형식 매개 변수 제약 시키는 방법
where 형식매개변수 : 제약 조건

// 형식 매개 변수 제약 시키기 예시
clss MyList<T> where T : MyClass
{
    // 내부 구현
}

void CopyArray<T>(T[] source, T[] target) where T : struct
{
    // 내부 구현
}
  • 위와 같이 where절을 추가해 주면 클래스나 메소드에 해당 타입에 대한 제약이 생긴다.
  • where절에 쓸 수 있는 제약 조건은 아래 표와 같다.
제약 설명
where T : struct T는 값 형식이어야 한다.
where T : class T는 참조 형식이어야 한다.
where T : new() T는 매개 변수가 없는 생성자를 포함하여야 한다.
where T : 기반클래스이름 T는 명시한 기반 클래스의 파생 클래스여야 한다.
where T : 인터페이스이름 T는 명시한 인터페이스를 구현해야 한다. 인터페이스는 여러 개를 명시할 수 있다.
where T : U T는 또 다른 형식 매개 변수 U로부터 상속받은 클래스여야 한다.

일반화 컬렉션

  • List<T>, Queue<T>, Stack<T>, Dictionary<TKey, TValue>는 각각 ArrayList, Queue, Stack, Hashtable의 일반화 버전이다.
  • 일반화 컬렉션은 컴파일시 형식이 결정되기 때문에 쓸데 없는 형식 변환이 일어나지 않아 object 형식 기반 컬렉션에 비해 나은 성능을 갖는다. 더불어 잘못된 객체를 담을 위험도 피할 수 있다.
  • (실제 클래스 만드는 예시는 생략)

foreach를 사용할 수 있는 일반화 클래스

  • IEnumerable, IEnumerator 인터페이스를 상속하여 메소드와 프로퍼티를 구현하면 foreach를 이용할 수 있지만, 이 경우 형식 변환이 발생한다는 문제가 생긴다.
  • IEnumerable<T>, IEnumerator<T> 인터페이스를 상속하여 메소드와 프로퍼티를 구현하면 형식 변환으로 인한 성능 저하가 없으면서 foreach 순회가 가능한 클래스를 작성할 수 있다.
  • (실제 객체 만드는 예시는 생략)

IEnumerable<T>의 메소드

메소드 설명
IEnumerator GetEnumerator() IEnumerator 형식의 객체를 반환(IEnumerator로부터 상속받은 메소드)
IEnumerator<T> GetEnumerator() IEnumerator<T> 형식의 객체를 반환

IEnumerator<T>의 메소드와 프로퍼티

메소드 설명
boolean MoveNext() 다음 요소로 이동. 컬렉션의 끝을 지난 경우에는 false, 이동이 성공한 경우에는 true를 반환한다.
void Reset() 컬렉션의 첫 번째 위치의 “앞”으로 이동. 첫 번째 위치가 0번 이라면 Reset()을 호출하면 -1로 이동하게 된다. 첫 번째 위치로의 이동은 MoveNext()를 호출한 다음에 이루어진다.
Object Current { get; } 컬렉션의 현재 요소를 반환한다.(IEnumerator로부터 상속받은 프로퍼티)
T Current { get; } 컬렉션의 현재 요소를 반환한다.

뇌를 자극하는 C# 4.0 프로그래밍/ 배열과 컬렉션, 그리고 인덱서

All for one, one for all

// 배열 선언 방법
데이터형식[] 배열이름 = new 데이터형식[용량];
  • 배열은 같은 타입의 변수 여러 개를 한 번에 다룰 수 있기 때문에 편리하고 가독성도 좋다.

배열을 초기화하는 방법 세 가지

// 배열의 용량을 명시
string[] array1 = new string[3]{ "안녕", "hello", "halo" };

// 배열의 용량을 생략
string[] array2 = new string[]{ "안녕", "hello", "halo" };

// 데이터를 대입
string[] array2 = { "안녕", "hello", "halo" };

알아 두면 삶이 윤택해지는 System.Array

분류 이름 설명
정적 메소드 Sort() 배열을 정렬
정적 메소드 BinarySearch() 이진 탐색을 수행
정적 메소드 IndexOf() 배열에서 찾고자 하는 특정 데이터의 인덱스를 반환
정적 메소드 TrueForAll() 배열의 모든 요소가 지정한 조건에 부합하는지 여부를 반환
정적 메소드 FindIndex() 배열에서 지정한 조건에 부합하는 첫 번째 요소의 인덱스를 반환. IndexOf()가 특정 값을 찾는데 비해, FindIndex()는 지정한 조건에 바탕하여 값을 찾는다.
정적 메소드 Resize() 배열의 크기를 재조정
정적 메소드 Clear() 배열의 모든 요소를 초기화
정적 메소드 ForEach() 배열의 모든 요소에 대해 동일한 작업을 수행
인스턴스 메소드 GetLength() 배열에서 지정한 차원의 길이를 반환
프로퍼티 Length 배열의 길이를 반환
프로퍼티 Rank 배열의 차원을 반환

2차원 배열

// 2차원 배열 선언 방법
데이터형식[ , ] 배열이름 = new 데이터형식[2차원길이, 1차원길이];

// 2차원 배열 초기화 방법
// 배열의 형식과 길이를 명시
int[,] arr = new int[2, 3]{ { 1, 2, 3 }, { 4, 5, 6 } };

// 배열의 길이를 생략
int[,] arr = new int[,]{ { 1, 2, 3 }, { 4, 5, 6 } };

// 데이터를 대입
int[,] arr = { { 1, 2, 3 }, { 4, 5, 6 } };

다차원 배열

// 다차원 배열 선언 예시
int[, ,] array = new int[4, 3, 2]
{
    { { 1, 2 }, { 3, 4 }, { 5, 6 } },
    { { 1, 4 }, { 2, 5 }, { 3, 6 } },
    { { 6, 5 }, { 4, 3 }, { 2, 1 } },
    { { 6, 3 }, { 5, 2 }, { 4, 1 } },
}
  • 3차원 이상의 다차원 배열은 가능하면 쓰지 말자.

가변 배열

// 가변 배열 선언 방법
데이터형식[][] 배열이름 = new 데이터형식[가변배열의 용량][];

// 선언 예시
int[][] jagged = new int[3][];
jagged[0] = new int[5] { 1, 2, 3, 4, 5 };
jagged[1] = new int[] { 10, 20, 30 };
jagged[2] = new int[] { 100, 200 };
  • 가변 배열의 요소로 입력되는 배열은 그 길이가 모두 같을 필요가 없다. 그래서 가변배열 –들쭉날쭉한– 이라고 부른다.
  • 가변 배열은 다차원 배열과 달리 배열을 요소로써 접근할 수 있다.

컬렉션 맛보기

ArrayList

// ArrayList 사용 예시
ArrayList list = new ArrayList();
list.Add(10);
list.Add(20);
list.Add(30);

list.RemoveAt(1); // 20을 삭제

list.Insert(25, 1) // 25를 1번 인덱스에 삽입

Queue

// Queue 사용 예시
Queue que = new Queue();
que.Enqueue(1);
que.Enqueue(2);
que.Enqueue(3);

int a = que.Dequeue(); // 제일 앞에 있는 1이 빠져서 a에 대입된다.
  • Queue는 작업을 차례대로 입력해 뒀다가 입력된 순서대로 하나씩 꺼내 처리하는 자료 구조이다.
  • 배열이나 리스트가 원하는 위치에 자유롭게 접근하는 반면 Queue는 입력은 오로지 뒤에서, 출력은 오로지 앞에서만 이루어진다.
    • Queue는 OS에서 CPU가 처리해야 할 작업을 정리할 때, 프린터가 여러 문서를 출력할 때, 인터넷 동영상 스트리밍 서비스에서 컨텐츠를 버퍼링할 때 등과 같은 많은 곳에서 사용되는 자료 구조이다.

Stack

// Stack 사용 예시
Stack stack = new Stack();
stack.Push(1); // 최상위 데이터는 1
stack.Push(2); // 최상위 데이터는 2
stack.Push(3); // 최상위 데이터는 3

int a = stack.Pop(); // 제일 위에 있는 3이 빠져서 a에 대입되고 최상위 데이터는 2가 됨.
  • Stack은 Queue와 반대로 먼저 들어온 데이터가 나중에 나가고(First In-Last Out), 나중에 들어온 데이터는 먼저 나가는(Last In-First Out) 자료 구조이다.

Hashtable

// Hashtable 사용 예시
Hashtable ht = new Hashtable();
ht["book"] = "책";
ht["cook"] = "요리사";
ht["tweet"] = "지저귐";

Console.WriteLine( ht["book"] );
Console.WriteLine( ht["cook"] );
Console.WriteLine( ht["tweet"] );
  • Hashtable은 키(key)와 값(value)의 쌍으로 이루어진 데이터를 다룰 때 사용한다. ex) 사전
  • Hashtable은 탐색 속도도 빠르고 사용하는 것도 편리하다.

인덱서

// indexer 선언 방법
class 클래스 이름
{
    한정자 인덱서형식 this[형식 index]
    {
        get
        {
            // index를 이용하여 내부 데이터 반환
        }
        set
        {
            // index를 이용하여 내부 데이터 저장
        }
    }
}

// indexer 사용 예시
class MyList
{
    public int[] array;

    public MyList()
    {
        array new int[3];
    }

    public int this[int index]
    {
        get
        {
            return array[index];
        }
        set
        {
            if (index >= array.Length)
            {
                Array.Resize<int>(ref array, index+1);
                Console.WriteLine("Array Resized: {0}", array.Length);
            }

            array[index] = value;
        }
    }
}
  • 인덱서는 인덱스를 이용해서 객체 내의 데이터에 접근하게 해주는 프로퍼티와 같다. 프로퍼티가 이름을 통해 객체 내의 데이터에 접근하게 해 준다면, 인덱서는 인덱스를 통해 객체 내의 데이터에 접근하게 해준다.
  • (추가) static 클래스는 객체를 만들 수 없기 때문에 this 키워드를 사용할 수 없다. 따라서 static 클래스에서는 인덱서를 사용할 수 없다.

foreach가 가능한 객체를 만들어 보자

  • foreach 구문은 IEnumerable과 IEnumerator를 상속하는 형식만 지원한다.
    • IEnumerable과 IEnumerator의 메소드와 프로퍼티는 아래에 표로 정리
  • (실제 객체 만드는 예시는 생략)

IEnumerable의 메소드

메소드 설명
IEnumerator GetEnumerator() IEnumerator 형식의 객체를 반환

IEnumerator의 메소드와 프로퍼티

메소드 설명
boolean MoveNext() 다음 요소로 이동. 컬렉션의 끝을 지난 경우에는 false, 이동이 성공한 경우에는 true를 반환한다.
void Reset() 컬렉션의 첫 번째 위치의 “앞”으로 이동. 첫 번째 위치가 0번 이라면 Reset()을 호출하면 -1로 이동하게 된다. 첫 번째 위치로의 이동은 MoveNext()를 호출한 다음에 이루어진다.
Object Current { get; } 컬렉션의 현재 요소를 반환한다.

뇌를 자극하는 C# 4.0 프로그래밍/ 프로퍼티

메소드보다 프로퍼티

class MyClass
{
    private int myField;
    public int MyField
    {
        get
        {
            return myField;
        }
        set
        {
            myField = value;
        }
    }
}
  • 자바와 달리 C#에서는 프로퍼티라는 우아한 방법을 장치를 통해 필드의 은닉성을 보장한다.

자동 구현 프로퍼티

public class NameCard
{
    public string Name { get; set; }
    public string Number { get; set; }

    public string Date { get; private set; } // 이렇게 쓰면 읽기는 public, 쓰기는 private가 적용된다.
}
  • 일반적인 프로퍼티도 우아하지만 매번 필드와 프로퍼티를 따로 작성하는 것이 번거롭기 때문에 C# 3.0부터는 아예 자동구현 프로퍼티라고 해서 위와 같이 코드를 작성해도 프로퍼티로 인식해주고 있다.

프로퍼티와 생성자

public class BirthdayInfo
{
    public string Name { get; set; }
    public DateTime Birthday { get; set; }
    public int Age { get; set; }
}

BirthdayInfo birth = new BirthdayInfo()
                    {
                        Name = "수영", // 콤마(,)를 이용한다.
                        Birthday = new DateTime(1982, 4, 16)
                    };
  • 객체를 생성할 때 프로퍼티를 이용해서 초기화 할 수도 있다.
  • 초기화 할 때는 객체의 모든 프로퍼티를 초기화할 필요가 없기 때문에 생성자를 작성할 때와 달리 어떤 필드를 초기화 할 지 고민할 필요가 없다.

무명 형식

// 중괄호 사이에 임임의 프로퍼티 이름을 적고 값을 할당하면 그대로 새 형식의 프로퍼티가 된다.
var myInstance = new { Name = "박수영", Age = "34" };

// 이렇게 선언된 무명 형식의 인스턴스는 여느 객체처럼 프로퍼티에 접근하여 사용할 수 있다.
Console.WriteLine(myInstance.Name, myInstance.Age);
  • 무명 형식은 형식의 선언과 동시에 인스턴스를 할당한다. 이 때문에 인스턴스를 만들고 다시는 사용하지 않을 때 요긴하다.
  • 무명 형식의 프로퍼티에 할당된 값은 변경이 불가능하다. 인스턴스가 만들 때 값을 넣은 후에는 읽기만 가능하다는 이야기
  • 무명 형식은 LINQ와 함께 사용하면 매우 요긴하다.

인터페이스의 프로퍼티

interface IProduct
{
    string ProductName { get; set; }
}
  • 인터페이스에 들어가는 프로퍼티는 구현을 갖지 않기 떄문에 자동 구현 프로퍼티 선언과 모습이 같다.

추상 클래스와 프로퍼티

abstract class Product
{
    abstract public DateTime ProductDate { get; set; }
}
  • 추상 클래스는 그 특성상 구현된 프로퍼티와 구현되지 않은 프로퍼티를 모두 가질 수 있다. 추상 클래스에서 구현되지 않은 프로퍼티를 만들 때는 abstract 한정자를 붙여서 만들면 된다.

뇌를 자극하는 C# 4.0 프로그래밍/ 인터페이스와 추상 클래스

인터페이스의 선언

interface 인터페이스이름
{
    반환형식 메소드이름1 (매개변수목록);
    반환형식 메소드이름2 (매개변수목록);
    반환형식 메소드이름3 (매개변수목록);
}
  • 인터페이스는 interface 키워드를 이용해서 선언한다.
  • 인터페이스는 메소드, 이벤트, 인덱서, 프로퍼티만 가질 수 있으며 모든 멤버는 public으로 선언된다.
  • 인터페이스에 선언되는 메소드는 구현이 없다.
  • 인터페이스는 인스턴스를 만들 수 없다.
  • 인터페이스를 상속 받는 파생 클래스는 인터페이스에 선언되어 있는 모든 메소드를 구현해줘야 하며, 이 메소드들은 public 한정자로 수식해야 한다.

인터페이스는 약속이다

  • 인터페이스는 소프트웨어 내에서 USB와 같은 역할을 한다. 클래스가 따라야 하는 약속이 되는 셈. 이 약속은 인터페이스로부터 파생될 클래스가 어떤 메소드를 구현해야 하는지를 정의한다.

인터페이스를 상속하는 인터페이스

interface 파생인터페이스 : 부모인터페이스
{
    // 멤버
}

여러 개의 인터페이스, 한꺼번에 상속하기

  • 클래스는 여러 클래스를 한꺼번에 상속할 수 없는데 이는 “죽음의 다이아몬드” 문제 때문이다. 이러한 이유로 C#은 클래스의 다중상속을 아예 허용하지 않는다.
  • 인터페이스는 구현이 없기 때문에 죽음의 다이아몬드 문제가 발생하지 않으며, 다중상속에 자유롭다.

포함(Containment) 기법

MyVehicle()
{
    Car car = new Car();
    Plane plane = new Plane();

    public void Run ()
    {
        car.Ride();
    }

    public void Fly ()
    {
        plane.Ride();
    }
}
  • 포함이라는 기법을 이용하면 상속을 쓰지 않고도 다른 클래스의 기능을 새로운 클래스에 넣을 수 있다. 이는 위 코드와 같이 클래스 안에 물려 받고 싶은 기능을 가진 클래스들을 필드를 선언해 넣으면 된다.

추상 클래스: 인터페이스와 클래스의 차이

abstract class 클래스이름
{
    // 클래스와 동일하게 구현

    // 추상 메소드 선언
    public abstract void Method();
}
  • 추상 클래스는 인터페이스와 달리 구현을 가질 수 있다. 그러나 클래스와 달리 인스턴스를 가질 수는 없다.
  • 추상 클래스는 추상 메소드를 가질 수 있다. 추상 메소드는 구현을 갖지는 못하지만 파생 클래스에서 반드시 구현되도록 강제한다. 이 부분은 인터페이스와 비슷하다.
  • C# 컴파일러는 추상 메소드가 public, protected, internal, protected internal 한정자 중 하나로 수식될 것을 강요한다.

뇌를 자극하는 C# 4.0 프로그래밍/ 클래스

객체 지향 프로그래밍과 클래스

int a = 30;
  • 위 코드에서 int는 클래스이고 a는 실제 데이터를 담을 수 있는 인스턴스(객체)이다.
  • 객체로 뽑아낸 속성과 기능은 클래스 안에 각각 변수와 메소드로 표현된다.

클래스의 선언과 객체의 생성

// 고양이 클래스 만들기
class Cat
{
    public string Name;
    public string Color;

    public void Meow ()
    {
        Console.WriteLine("{0} : 야용", Name);
    }
}

// 고양이 클래스의 인스턴스 생성
Cat kitty = new Cat();
Cat persian;
  • Name이나 Color처럼 클래스 안에 선언된 변수를 필드(Field)라고 하며, 필드와 메소드, 프로퍼티, 이벤트 등 클래스 내에 선언되어 있는 요소들을 멤버(Member)라고 한다.
  • 위 코드에서 Cat()은 생성자라고 부른다. 생성자는 클래스와 동일한 이름을 가지며 객체를 생성하는 역할을 한다.
  • new 키워드는 생성자를 호출해서 객체를 생성하는데 사용하는 연산자이다.
  • 모든 클래스는 복합 데이터 형식이고 참조 형식이다. 위와 같은 선언문에서 persian는 null이 된다. persian 자체에 메모리가 할당되는 것이 아니고 persian는 참조로써 객체가 있는 곳을 가리킬 뿐이기 때문.
  • 이러한 이유에서 new 연산자와 생성자가 필요하다. new 연산자와 생성자를 이용해서 힙에 객체를 생성하고, kitty는 생성자가 힙에 생성한 객체를 가리키게 된다.

객체의 삶과 죽음에 대하여: 생성자와 소멸자

  • 객체는 생성자(Constructor)에 의해 만들어지고 소멸자(Destructor)에 의해 파괴된다.

생성자

class 클래스이름
{
    한정자 클래스이름 (매개변수목록)
    {
    }

    // 필드
    // 메소드
}
  • 생성자의 임무는 단 한가지, 해당 형식(클래스)의 객체를 생성하는 역할만 수행한다.
  • 클래스를 선언할 때 명시적으로 생성자를 구현하지 않아도 컴파일러에서 생성자를 만들어준다. 만일 프로그래머가 생성자를 하나라도 직접 정의하면 C# 컴파일러는 매개 변수 없는 기본 생성자를 제공하지 않게 된다.
  • 생성자는 오버로딩이 가능하기 때문에 다양한 버전의 생성자를 준비해 놓을 수 있다.

소멸자

class 클래스이름
{
    ~클래스이름 ()
    {
    }

    // 필드
    // 메소드
}
  • 소멸자는 클래스 이름 앞에 ~을 붙인 형태를 취한다.
  • 소멸자는 생성자와 달리 매개 변수도 없고 한정자도 사용하지 않으며 오버로딩도 불가능하고 직접 호출할 수도 없다.
  • 소멸자는 CLR의 가비지 컬렉터가 객체가 소멸되는 시점을 판단해서 소멸자를 호출해 준다.
  • 소멸자는 다음과 같은 이유로 사용하지 않는 것이 권장된다.
    1. CLR의 가비지 컬렉터가 언제 동작할지 완벽하게 예측할 수 없다.
    2. 명시적으로 소멸자가 구현되어 있으면 가비지 컬렉터가 object로 부터 상속받은 Finalize() 메소드를 클래스의 족보를 타고 올라가며 호출하기 때문에 대개의 경우 프로그램 성능 저하만 가져올 확률이 높다.
    3. CLR의 가비지 컬렉터가 우리보다 더 똑똑하게 객체의 소멸을 처리할 수 있다. 생성자는 생성자에게, 소멸은 가비지 컬렉터에 맡기는 편이 낫다.

객체 복사하기: 얕은 복사와 깊은 복사

MyClass source = new MyClass();
source.Field1 = 10;
source.Field2 = 20;

MyClass target = source;
target.Field2 = 30;
  • 위와 같이 코드를 짰다면 source의 Field2 값에 30이 들어가게 된다. 이유는 클래스가 참조 형식이기 때문.

  • 이와 같이 참조만 살짝 복사하는 것을 얕은 복사(Shallow Copy)라고 한다.
  • 만일 아래 이미지와 같은 깊은 복사(Deep Copy) –target이 source의 데이터를 복사하여 별도의 힙 공간에 객체를 보관하려면– 를 하려면 다음과 같은 코드를 짜야한다. 참고로 C#에서는 이와 같은 일을 자동으로 해주는 구문은 제공해 주지 않는다.
    • (추가) C#에서 깊은 복사를 할 때는 클래스가 ICloneable을 상속 받아 Clone()이라는 메서드를 직접 구현하는 식으로 권장된다. 클래스의 내부 구조는 작성자만 알 뿐, 프로그램 차원에서는 알 수 없어서 이렇게 하는 것 같다.

Class MyClass
{
    public int Field1;
    public int Field2;

    // 객체를 힙에 새로 할당해서 그곳에 자신의 멤버를 일일이 복사해 넣는다.
    public MyClass DeepCopy()
    {
        MyClass newCopy = new MyClass();

        newCopy.Field1 = this.Field1;
        newCopy.Field2 = this.Field2;

        return newCopy;
    }
}

this 키워드

this() 생성자

class MyClass
{
    int a, b, c;

    public MyClass()
    {
        this.a = 5235;
    }

    public MyClass(int b)
    {
        this.a = 5235;
        this.b = b;
    }

    public MyClass(int b, int c)
    {
        this.a = 5235;
        this.b = b;
        this.c = c;
    }
}

// this()를 이용한 버전
class MyClass
{
    int a, b, c;

    public MyClass()
    {
        this.a = 5235;
    }

    public MyClass(int b) : this()
    {
        this.b = b;
    }

    public MyClass(int b, int c) : this()
    {
        this.c = c;
    }
}
  • this가 객체 자신을 지칭하는 키워드인 것처럼 this()는 자기 자신의 생성자를 가리킨다.
  • this()는 생성자에서만 사용될 수 있으며 생성자의 코드 블록 안쪽이 아닌 앞쪽에서만 사용 가능하다.

접근 한정자로 공개 수준 결정하기

접근 한정자 설명
public 클래스의 내부/ 외부 모든 곳에서 접근 가능.
protected 클래스 외부에서는 접근할 수 없지만, 파생 클래스에서는 접근 가능.
private 클래스의 내부에서만 접근 가능.
internal 같은 어셈블리 코드에 대해서만 public으로 접근 가능. 다른 어셈블리 코드에서는 private과 같다.
protected internal 같은 어셈블리 코드에 대해서만 protected으로 접근 가능. 다른 어셈블리 코드에서는 private과 같다.

상속으로 코드 재활용하기

class 기반 클래스
{
    // 멤버 선언
}

class 파생 클래스 : 기반 클래스
{
    // 아무 멤버를 선언하지 않아도 기반 클래스의 모든 것을 물려 받는다.
    // 단, private 멤버는 제외.
}
  • 파생 클래스는 객체를 생성할 때 내부적으로 기반 클래스의 생성자를 호출한 후에 자신의 생성자를 호출하고, 객체가 소멸될 때는 반대싀 순서로 소멸자를 호출한다.
  • this 키워드가 자기 자신을 가리키는 것처럼 base 키워드는 기반 클래스를 가리킨다.
  • 같은 원리로 base()는 기반 클래스의 생성자가 된다.

상속 봉인

sealed class Base
{
    // 이 클래스는 상속을 허용하지 않는다.
}
  • 클래스의 상속을 막고 싶다면 sealed 한정자를 클래스 앞에 붙이면 된다.

기반 클래스와 파생 클래스 사이의 형식 변환, 그리고 is와 as

연산자 설명
is 객체가 해당 형식에 해당하는지를 검사하여 그 결과를 bool로 반환.
as 형식 변환 연산자와 같은 역할을 한다. 다만 형변환 연산자가 변환에 실패하는 경우 예외를 던지는 반면 as 연산자는 객체 참조를 null로 만든다.
  • 일반적으로 형식 변환 연산자 대신 as를 사용하는 쪽이 권장된다. 형식 변환에 실패해도 예외가 일어나 코드가 점프하는 일이 없으므로 코드를 고나리하기 더 수월하기 때문. 단, as는 참조 형식에 대해서만 사용 가능하다.

오버라이딩과 다형성

class ArmorSuite
{
    public virtual void Initialize()
    {
        Console.WriteLine("Armored");
    }
}

class IronMan : ArmorSuite
{
    public override void Initialize()
    {
        base.Initialize();
        Console.WriteLine("Repulsor Rays Armored");
    }
}
  • 객체 지향 프로그래밍에서 다형성(Polymorphism)이란 하위 형식 다형성(Subtype Polymorphism)의 준말이다. 다시 말해 자신으로부터 상속받아 만들어진 파생 클래스를 통해 다형성을 실현한다는 뜻이다.
  • 파생 클래스에서 기반 클래스의 메소드를 오버라이딩 하려면 기반 클래스에서 오버라이딩 될 메소드를 virtual로 선언해 두어야 한다.

메소드 숨기기

class Base
{
    public void MyMethod()
    {
        Console.WriteLine("Base MyMethod");
    }
}

class Derived : Base
{
    public new void MyMethod()
    {
        Console.WriteLine("Derived MyMethod");
    }
}
  • 메소드 숨기기란, CLR에게 기반 클래스에서 구현된 버전의 메소드를 감추고 파생 클래스에서 구현된 버전만을 보여 주는 것을 말한다.
  • 메소드 숨기기는 완전한 다형성을 표현하지 못하는 한계가 있다.

오버라이딩 봉인하기

class Base
{
    public virtual void SealMe()
    {
        //
    }
}

class Derived : Base
{
    public sealed void MyMethod()
    {
        //
    }
}
  • 클래스를 상속이 안 되도록 봉인하는 것처럼 메소드도 오버라이딩이 되지 않도록 봉인할 수 있다. 그렇다고 모든 메소드를 봉인할 수 있는 것은 아니고, virtual로 선언된 가상 메소드를 오버라이딩한 버전의 메소드만 가능하다.

중첩 클래스

  • 클래스 안에 클래스를 선언하면 중첩 클래스가 된다.

분할 클래스

partial class MyClass
{
    public void Method1() {}
    public void Method2() {}
}

partial class MyClass
{
    public void Method3() {}
    public void Method4() {}
}

MyClass obj = new MyClass();
obj.Method1();
obj.Method2();
obj.Method3();
obj.Method4();
  • partial 한정자를 사용하면 클래스를 분할할 수 있다. 클래스 분할은 클래스 구현이 길어질 경우 여러 파일에 나눠서 구현할 수 있게 함으로서 소스 코드 관리의 편의를 높이는데 목적이 있다.

확장 메소드

namespace 네임스페이스이름
{
    public static class 클래스이름
    {
        public static 반환형식 메소드이름(this 대상형식 식별자, 매개변수목록)
        {
            // 구현
        }
    }
}

namespace MyExtension
{
    public static class IntegerExtension
    {
        public static int Power(this int myInt, int exponent)
        {
            // 구현 내용 생략
        }
    }
}

/* 확장 클래스 사용 방식 */
using MyExtension; // 확장 메소드를 담는 클래스의 네임스페이스를 사용한다.

int a = 2;
Console.WriteLine(a.Power(3)); // 마치 Power()가 원래부터 int 형식의 메소드였던 것처럼 사용할 수 있다.
Console.WriteLine(10.Power(4));
  • 확장 메소드(Extension Method)는 기존 클래스의 기능을 확장하는 기법이다. 확장 클래스를 이용하면 string 클래스나 int 형식에 새로운 기능을 넣을 수 있다.
  • 확장 메소드는 static 한정자로 선언하고 이 메소드의 첫 번째 매개 변수는 반드시 this 키워드와 함께 확장하고자 하는 클래스(형식)의 인스턴스여야 한다.
  • 확장 메소드를 담는 클래스 또한 static 한정자로 수식해야 한다.

구조체

특징 클래스 구조체
키워드 class struct
형식 참조 형식 값 형식
복사 얕은 복사 깊은 복사
인스턴스 생성 new 연산자와 생성자 필요 선언만으로도 생성
생성자 매개 변수 없는 생성자 선언 가능 매개 변수 없는 생성자 선언 불가능
상속 가능 모든 구조체는 System.Object 형식을 상속하는 System.ValueType으로부터 직접 상속 받음
  • 구조체는 struct 키워드를 이용해서 선언한다.
  • 구조체와 클래스의 가장 큰 차이는 클래스는 참조 형식이고 구조체는 값 형식이라는 점.
  • 구조체는 값 형식이므로 인스턴스의 사용이 끝나면 즉시 메모리에서 제거되기 때문에 클래스에 비해 성능상 이점을 갖는다.
  • 구조체는 매개 변수가 없는 생성자는 선언할 수 없다.
  • 구조체의 각 필드는 CLR이 기본값으로 초기화 해준다.

뇌를 자극하는 C# 4.0 프로그래밍/ 메소드로 코드 간추리기

메소드란?

  • 메소드가 함수, 프로시져, 서브루틴 등과 다른 점은 클래스 안에 존재한다는 것.

return에 대하여

  • return문은 메소드를 종결시키고 프로그램의 흐름을 호출자에게 돌려준다.

재귀 호출(Recursive Call)

메소드가 자기 자신을 스스로 호출하는 것. 재귀 호출은 코드를 단순하게 구성할 수 있다는 장점이 있는 한편 성능에 나쁜 영향을 주기 때문에 주의해서 사용해야 한다.

매개 변수에 대하여

  • 매개 변수도 메소드 외부에서 메소드 내부로 데이터를 전달하는 매개체 역할을 할 뿐이지 근본적으로는 변수이기 때문에 한 변수를 또 다른 변수에 할당하면 그 데이터가 값형식이든 참조형식이든 상관 없이 변수가 담고 있는 데이터만 복사된다.
  • 이와 같이 메소드를 호출할 때 데이터를 복사해서 매개 변수에 넘기는 것을 “값에 의한 전달(Call by value)”라고 한다.

참조에 의한 매개 변수 전달

int x = 3;
int y = 4;
Swap(ref x, ref y);

void Swap (ref int a, ref int b)
{
    int temp = b;
    b = a;
    a = temp;
}
  • 매개 변수를 “참조에 의한 전달(Call by reference)”로 넘기면 매개 변수가 메소드에 넘겨진 원본 변수를 직접 참조하게 된다. 따라서 메소드 안에서 매개 변수를 수정하면 이 매개 변수가 참조하고 있는 원본 변수에 수정이 이루어지게 된다.
  • 참조에 의한 매개 변수 전달은 ref 키워드를 사용하면 된다.

출력 전용 매개 변수

iint a = 20;
int b = 3;
int c;
int d;
Divide(a, b, out c, out d);

void Divide (int a, int b, out int quotient, out int remainder)
{
    quotient = a / b;
    remainder = a % b;
}
  • 결과를 2개 이상 반환하는 메소드를 만들고자 할 때 ref 키워드를 이용하면 된다. 그런데 C#에서는 out 이라는 보다 안전한 방법을 제공하고 있으므로 그것을 사용하면 좋다.
  • out 키워드를 이용해서 변수를 넘길 때는 메소드가 해댕 매개 변수에 결과를 저장하지 않으면 컴파일러가 에러를 출력한다. 또한 호출된 메소드에서는 입력된 out 매개 변수를 “읽을” 수 없고 오직 “쓰기”만 가능하다. 출력 전용 매개 변수를 다른 용도로 사용하는 것을 금지하는 것.

메소드 오버로딩

int Plus (int a, int b)
{
    return a + b;
}

double Plus (double a, double b)
{
    return a + b;
}

// 위의 매개 변수를 double로 받는 것은 일반화 프로그래밍을 하는 편이 나아 보여서 매개변수 개수와 형식이 다른 버전을 별도로 추가하였다.
int Plus (int a, long b, double c)
{
    return a + b + c;
}
  • 메소드 오버로딩이란 하나의 메소드 이름에 여러 개의 구현을 올리는 것을 의미한다.
  • 이런 식으로 오버로딩을 해 놓으면 컴파일러가 메소드 호출 코드에 사용되는 매개 변수의 수와 형식을 분석해서 –오로지 매개 변수만 분석하며 반환 형식은 따지지 않는다– 어떤 버전이 호출될 지를 찾아 준다. 실행할 메소드의 버전을 찾는 작업이 컴파일 타임에 이루어지므로 성능 저하는 없다.

가변길이 매개 변수

int total = 0;

total = Sum(1, 2);
total = Sum(1, 2, 3);
total = Sum(1, 2, 3, 4, 5, 6, 7, 8, 9);

int Sum (params int[] args)
{
    int sum = 0;

    for (int i = 0 ; i < args.Length ; i++ )
    {
        sum += args[i];
    }

    return sum;
}
  • 가변 길이 매개 변수란 개수가 유연하게 변할 수 있는 매개 변수로 이를 이용하면 모든 매개 변수의 합을 구하는 메소드를 따로 오버로딩 하여 구현하지 않아도 된다.
  • 가변길이 매개변수는 변수의 형식이 같은 경우에만 유효하므로 변수의 형식이 달라지는 경우는 오버로딩을 해야 한다.

명명된 매개 변수

PrintProfile(name: "박수영", phone: "010-1234-5678");

void PrintProfile (string name, string phone)
{
    Console.WriteLine("Name: {0}, Phone: {1}", name, phone);
}
  • C# 에서는 명명된 매개 변수(Named Parameter)를 이용해서 매개 변수에 데이터를 할당할 수 있다.
  • 명명된 매개 변수를 이용하면 코드 가독성도 좋아지며 매개 변수가 많아졌을 때 순서가 꼬여 발생할 수 있는 오류를 줄일 수 있다는 점에서 좋은 방법이라 할 수 있다.

선택적 매개 변수

MyMethod();
MyMethod(1);
MyMethod(1, 2);

void MyMethod (int a = 0, int b = 0)
{
    Console.WriteLine("{0}, {1}", a, b);
}
  • 메소드의 매개 변수는 위 코드와 같이 기본값을 가질 수 있다. 이러한 기본값을 가지는 매개 변수는 필요에 따라 데이터를 할당하거나 할당하지 않을 수 있기 때문에 “선택적 매개 변수(Optional Parameter)”라고 부른다.
  • 선택적 매개 변수는 항상 필수 매개 변수 뒤에 와야 한다.
  • 선택적 매개 변수는 편의성이 있긴 하지만 모호함도 함께 발생하므로 주의해서 사용할 필요가 있다. 사용할 때는 명명된 매개 변수와 함께 사용하면 낫다.