안녕하세요. 오늘은 C# Delegate라는 것을 알아볼 것 입니다.


C# Delegate는 한 마디로 요약하자면 메소드(함수)를 변수로 저장하는 것 입니다.

C/C++을 해보셨던 분들에게는 함수 포인터와 비슷하다고 생각하시면 될 것 입니다.


일단 아래는 C# 델리게이트를 선언하는 코드입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 델리게이트는 아래와 같이 선언합니다.
public delegate void NoneReturn_NoneParam_Delegate();
static void NoneReturn_NoneParam_Func()
{
    Console.WriteLine("NoneReturn_NoneParam_Func");
}
 
public delegate int IntReturn_NoneParam_Delegate();
static int IntReturn_NoneParam_Func()
{
    Console.WriteLine("IntReturn_NoneParam_Func");
    return 10;
}
 
public delegate float FloatReturn_NoneParam_Delegate();
static float FloatReturn_NoneParam_Func()
{
    Console.WriteLine("FloatReturn_NoneParam_Func");
    return 10.0f;
}
 
public delegate void NoneReturn_OneIntParam_Delegate(int i);
static void NoneReturn_OneIntParam_Func(int i)
{
    Console.WriteLine("NoneReturn_OneIntParam_Func");
}
 
public delegate int IntReturn_OnefloatParam_Delegate(float i);
static int IntReturn_OnefloatParam_Func(float f)
{
    Console.WriteLine("IntReturn_OnefloatParam_Func");
    return 0;
}
 
public delegate float FloatReturn_OneIntParam_Delegate(int i);
static float FloatReturn_OneIntParam_Func(int i)
{
    return 10f;
}
 
cs


보시면 

"접근 한정자 + delegate + 함수 타입 " 방식으로 선언합니다.


예를 들어 

2 번째 줄에 선언된 delegate의 타입 이름은 NoneReturn_NoneParam_Delegate 이며

저 타입에 들어갈 수 있는 함수의 형식은 반환 값과 매개변수가 없는 함수만 저 형식에

저장 될 수 있는 것이죠.


그럼 이제 저 delegate를 사용하는 코드를 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 선언한 델리게이트를 사용한 변수입니다.
NoneReturn_NoneParam_Delegate noneReturn_NoneParam 
    = NoneReturn_NoneParam_Func;
 
IntReturn_NoneParam_Delegate intReturn_NoneParam 
    = IntReturn_NoneParam_Func;
 
FloatReturn_NoneParam_Delegate floatReturn_NoneParam 
    = FloatReturn_NoneParam_Func;
 
NoneReturn_OneIntParam_Delegate noneReturn_OneIntParam 
    = NoneReturn_OneIntParam_Func;
 
IntReturn_OnefloatParam_Delegate intReturn_OnefloatParam 
    = IntReturn_OnefloatParam_Func;
 
FloatReturn_OneIntParam_Delegate floatReturn_OneIntParam 
    = FloatReturn_OneIntParam_Func;
 
cs


선언한 델리게이트를 사용하는 법은 일반 변수랑 같습니다.


예를들어 4 번째 줄을 보시면


NoneReturn_NoneParam_Delegate noneReturn_NoneParam 

                = NoneReturn_NoneParam_Func;


"델리게이트 타입 이름 + 변수 명 + 대입할 함수 이름" 입니다.


그럼 저렇게 변수에 저장 된 함수를 호출해볼까요?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 호출 방법 1
if (noneReturn_NoneParam != null)
    noneReturn_NoneParam();
 
if (intReturn_NoneParam != null)
    intReturn_NoneParam();
 
if (floatReturn_NoneParam != null)
    floatReturn_NoneParam();
 
if (noneReturn_OneIntParam != null)
    noneReturn_OneIntParam(5);
 
if (intReturn_OnefloatParam != null)
    intReturn_OnefloatParam(5f);
 
if (floatReturn_OneIntParam != null)
    floatReturn_OneIntParam(5);
 
 
 
// 호출 방법 2
// '?' 은 만약 변수가 NULL이 아니라면 뒤 함수를 호출한다는 뜻입니다.
noneReturn_NoneParam?.Invoke();
intReturn_NoneParam?.Invoke();
floatReturn_NoneParam?.Invoke();
 
noneReturn_OneIntParam?.Invoke(5);
intReturn_OnefloatParam?.Invoke(5f);
floatReturn_OneIntParam?.Invoke(5);
 
cs


호출 방법 1


함수를 저장해놓은 변수가 NULL 인지 체크하고 일반 함수를 호출하는 것과 같은 방식으로 변수에 저장되어있는 함수를 호출합니다.


항상 NULL 체크를 하는 것이 좋습니다. 만약 체크를 안하고 NULL인 델리게이트 변수의 저장된 함수를 호출하면 예외가 터집니다.


호출 방법2


이 방식은 델리게이트 타입의 변수에는 자동으로 Invoke라는 함수가 생기게 됩니다. 

Invoke를 호출하면 해당 함수를 호출하는 것이지요.


근데


변수 이름 옆에 붙은 '?' 의 의미는 해당 변수가 NULL 이 아닐 경우(변수가 초기화 된 경우) Invoke를 호출하라는 의미입니다. 간단하죠? 만약 변수가 NULL 이면 Invoke가 호출되지 않습니다. 저 '?' 같은 경우는 델리게이트 뿐만 아니라 다른 클래스에도 사용 가능 하니 한번 사용해보세요.



오늘은 C#의 예외처리에 대해 알아보겠습니다. 


예외를 적절하게 사용하면 프로그램 실행 중간에 에러가 터져도 프로그램이 꺼지지 않고 넘길 수 있습니다. 하지만 굳이 예외가 필요없는 곳인데도 불구하고 과도하게 사용 할 경우 성능 저하를 불러 올 수 있습니다.


다음과 같은 형식으로 예외 처리를 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void Main(string[] args)
{
    // try 문 안에서 예외를 던져야 예외를 catch 할 수 있습니다.
    try
    {
        // 생성자에 Message를 넣을 수 있습니다.
        throw new Exception("예외를 던집니다.");
    }
    // try 문 안에서 throw 된 예외를 catch 문에서 받습니다.
    catch (Exception ex)
    {
        // 예외 클래스의 Message 변수로 예외처리 
        //변수의 생성자에 넣은 메시지를 얻어 올 수 있습니다.
        Console.WriteLine(ex.Message);
    }
}
 
cs


출력 결과.


예외를 던집니다.

계속하려면 아무 키나 누르십시오 . . .


만약 throw 뒤에 다른 코드가 있다면 어떻게 될까요?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void Main(string[] args)
{
    // try 문 안에서 예외를 던져야 예외를 catch 할 수 있습니다.
    try
    {
        throw new Exception("예외를 던집니다.");
 
        Console.WriteLine("출력");
    }
    // try 문 안에서 throw 된 예외를 catch 문에서 받습니다.
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}
 
cs


출력 결과.


예외를 던집니다.

계속하려면 아무 키나 누르십시오 . . .


출력 결과를 보면 알 수 있듯이 try문 내에 예외가 던져진 곳 다음 코드는 실행이 안되는 것을 알 수 있습니다.


다음과 같은 상황에서는 어느 catch문이 실행될까요?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static void TestFunc()
    try
    {
        throw new Exception("예외");
    }
    catch (Exception ex)
    {
        Console.WriteLine("TestFunc " + ex.Message);
    }
 
}
 
static void Main(string[] args)
{
    // try 문 안에서 예외를 던져야 예외를 catch 할 수 있습니다.
    try
    {
        // 예외처리 구문이 있는 함수를 호출합니다.
        TestFunc();
    }
    // try 문 안에서 throw 된 예외를 catch 문에서 받습니다.
    catch (Exception ex)
    {
        Console.WriteLine("MainFunc" + ex.Message);
    }
}
 
cs


출력 결과.


TestFunc 예외

계속하려면 아무 키나 누르십시오 . . .


TestFunc 함수 내에 예외가 출력되었습니다. 결과를 보면 가장 가까운 catch (가까운 스택) 문에 점프하는 걸 볼 수 있습니다. 


근데 제가 처음에 너무 과도하게 사용하면 성능 저하가 있을 수도 있다고 적어놨습니다. 왜 그럴까요?


아래 코드를 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static void TestFunc01()
{
    TestFunc02();
 
}
 
static void TestFunc02()
{
    TestFunc03();
 
}
 
static void TestFunc03()
{
    TestFunc04();
}
 
static void TestFunc04()
{
    throw new Exception("예외");
}
 
 
static void Main(string[] args)
{
    // try 문 안에서 예외를 던져야 예외를 catch 할 수 있습니다.
    try
    {
        // 예외처리 구문이 있는 함수를 호출합니다.
        TestFunc01();
    }
    // try 문 안에서 throw 된 예외를 catch 문에서 받습니다.
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}
 
cs


위 코드의 콜 스택은 어떻게 될까요.


예외 발생 전

TestFunc01 -> TestFunc02 -> TestFunc03 -> TestFunc04 -> 예외 발생!


예외 발생 후

Test04 -> Test03 -> Test02 -> Test01 -> catch 문!.


예외가 발생하면 catch문이 있는 곳 까지 콜 스택이 되돌려집니다.

물론 위와 같은 코드는 한 번만 실행되기 때문에 성능 저하가 없을 수도 있습니다.


하지만 


서버 같이 항상 실행되는 경우 저런 예외처리 구문이 많으면 성능 저하가 생길 수 도 있습니다.


단 많은 .Net Framework에서는 예외처리를 사용하여 에러를 확인해 할 때가 있습니다.

예를 들면 C# System.Net에 서버 API들은 대부분의 오류를 예외처리로 던져줍니다. 


그렇기 때문에 신중하게 사용하는 것이 중요합니다.



이번에는 C#에서 열을 부분적으로 참조하는 법을 알아봅시다.


ArraySegment 를 이용하면 편리합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class TestClass
{
    /* 배열 참조하기 위한 정보가 들어있는 구조체이다.
     * 구조체 내부에는 
     * Array  : 버퍼를 참조하는 배열 변수.(제네릭으로 전달한 타입의 배열)
     * Offset : 그 배열의 참조 시작 위치를 알리는 int 형 변수
     * Count  : 몇개의 index를 참조하는지를 나타내는 int 형 변수.
    */
    ArraySegment<byte> _arraySegment;
    
    
    public void SetArray (byte[] inBuffer, int inOffset, int inCount)
    {
        _arraySegment = new ArraySegment<byte>(inBuffer, inOffset, inCount);
    }
 
    public void SetValue()
    {
        byte value = 0;
 
        // 참조한 배열의 시작 위치부터 참조의 끝까지 값을 세팅합니다.
        for (int i = _arraySegment.Offset; i < _arraySegment.Offset + _arraySegment.Count; ++i, ++value)
        {
            _arraySegment.Array[i] = value;
        }
    }
 
    public void ParintVluae()
    {
        // 참조한 배열의 시작 위치부터 참조의 끝까지 값을 출력합니다.
        for (int i = _arraySegment.Offset; i < _arraySegment.Offset + _arraySegment.Count; ++i)
        {
            Console.WriteLine(_arraySegment.Array[i]);
        }
    }
}
 
class Program
{
  
    static void Main(string[] args)
    {
        TestClass testClass = new TestClass();
        
        // 원본 버퍼를 생성한다.
        byte[] array = new byte[100];
 
        // 이렇게 할 경우 TestClass. _arraySegment는 array[50] 부터 10개만큼 참조한다.
        testClass.SetArray(array, 5010);
 
        testClass.SetValue();
 
        testClass.ParintVluae();
    }
 
cs


방법은 굉장히 간단합니다.


참조할려는 배열과 참조 시작 위치, 참조할 index의 갯수를 ArraySegment에 넘기면 됩니다.


사용할 때는 ArraySegment에 Offset과 Count를 활용해야합니다. 

*(TestClass. SetValue와 TestClass. PrintValue 참조)*


Offset : 참조 시작 위치.

Count : 참조 시작 위치 부터 몇개의 인덱스를 참조 할 것인가. 


*주의 할 점*

Offset과 Count를 잘 확인해서 배열의 참조한 부분보다 더 많은 부분을 접근하지 않도록 해야합니다.. 


만약 위 사항을 주의하지않고 다른 부분의 접근해버리면 다음과 같은 문제가 생깁니다.


1. 만약 배열의 크기가 넉넉하지 않고 잘못 접근한 부분이 배열의 할당된 것보다 크면 메모리 예외가 던져집니다. (따로 예외 처리를 안했을 경우 프로그램이 터져 버릴 수도 있습니다.)


2. 배열의 크기가 넉넉하더라고 만약 바로 잘못 접근한 부분을 다른 곳에서 참조하고있으면 그 부분의 원래 데이터가 회손될 수 있습니다.





'programing > C# Programming' 카테고리의 다른 글

C#Programming - Delegate  (0) 2019.10.07
C#Programming - C#의 예외처리(exception)  (0) 2019.09.23
C# Programming - C#에서의 배열 복사  (0) 2019.09.20

C# 에서의 배열 복사를 알아볼 것 입니다.

배열 복사를 할 땐 항상 배열의 인덱스의 범위를 벗어나서 복사 하지 않는지 주의해야합니다.


1. Array.CopyTo(Array destArray, int index) 

// srcArray의 데이터를 인자로 전달한 destArray에 저장합니다.

// destArray의 마지막 인자의 인덱스 부터 저장됩니다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
byte[] sourceArray = new byte[10]; // 복사를 할 배열입니다.
byte[] destinationArray = new byte[10]; // 복사를 당할 배열입니다.
 
// 복사 할 배열의 데이터를 삽입 합니다.
for (int i = 0; i < 10++i)
    sourceArray[i] = (byte)i;
 
// 복사하기전 복사를 당할 배열의 데이터를 출력해봅니다.
for (int i = 0; i < 10++i)
    Console.Write($"{destinationArray[i]} ");
// 출력 결과 : 0 0 0 0 0 0 0 0 0 0
 
// 복사합니다.
// 복사를 당할 배열, 복사할 크기 (index의 갯수)
sourceArray.CopyTo(destinationArray, 0);
 
Console.WriteLine();
 
// 복사한 후 복사를 당할 배열의 데이터를 출력해봅니다.
for (int i = 0; i < 10++i)
    Console.Write($"{destinationArray[i]} ");
// 출력 결과 : 0 1 2 3 4 5 6 7 8 9
 
Console.WriteLine();
 
cs


2. Array.Copy(Array srcArray, int srcCopyStartIndex, Array destArray, int destCopyStartIndex, int CopyLength)

// 예) srcArray[srcCopyStartIndex]에서 부터 CopyLength 까지 destArray에 복사합니다.

// destArray의 destCopyStartIndex부터 저장합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
byte[] sourceArray = new byte[10]; // 복사를 할 배열입니다.
byte[] destinationArray = new byte[10]; // 복사를 당할 배열입니다.
 
// 복사 할 배열의 데이터를 삽입 합니다.
for (int i = 0; i < 10++i)
    sourceArray[i] = (byte)i;
 
// 복사하기전 복사를 당할 배열의 데이터를 출력해봅니다.
for (int i = 0; i < 10++i)
    Console.Write($"{destinationArray[i]} ");
// 출력 결과 : 0 0 0 0 0 0 0 0 0 0
 
/* 복사합니다.
복사할 배열, 복사할 배열의 복사가 시작되는 위치,
복사를 당할 배열, 복사를 당할 배열의 저장 시작 위치,
크기 (index 갯수)
*/
Array.Copy(sourceArray, 0, destinationArray, 010);
Console.WriteLine();
 
// 복사한 후 복사를 당할 배열의 데이터를 출력해봅니다.
for (int i = 0; i < 10++i)
    Console.Write($"{destinationArray[i]} " );
// 출력 결과 : 0 1 2 3 4 5 6 7 8 9
 
Console.WriteLine();
 
cs


3. Array.Clone()

// 이 함수는 복사할 배열과 똑같은 복사본을 만듭니다. 할당도 알아서해줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
byte[] sourceArray = new byte[10]; // 복사를 할 배열입니다.
 
// 복사 할 배열의 데이터를 삽입 합니다.
for (int i = 0; i < 10++i)
    sourceArray[i] = (byte)i;
 
// sourceArray의 복사본을 똑같이 만듭니다. 할당도 알아서 해줍니다.
byte[] destinationArray = sourceArray.Clone() as byte[];
 
// 복사한 후 복사를 당할 배열의 데이터를 출력해봅니다.
for (int i = 0; i < 10++i)
    Console.Write($"{destinationArray[i]} ");
// 출력 결과 : 0 1 2 3 4 5 6 7 8 9
 
Console.WriteLine();
 
cs


4. Buffer.BlockCopy(Array srcArray, int srcOffset, Array destArray, int destOffset, int Length);

// Offset : 시작 위치입니다. 어디서부터 복사할지 결정 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
byte[] sourceArray = new byte[10]; // 복사를 할 배열입니다.
byte[] destinationArray = new byte[10]; // 복사를 당할 배열입니다.
 
// 복사 할 배열의 데이터를 삽입 합니다.
for (int i = 0; i < 10++i)
    sourceArray[i] = (byte)i;
 
// 복사하기전 복사를 당할 배열의 데이터를 출력해봅니다.
for (int i = 0; i < 10++i)
    Console.Write($"{destinationArray[i]} ");
// 출력 결과 : 0 0 0 0 0 0 0 0 0 0
 
/* 복사합니다. 
인자 (복사 할 배열, 복사할 배열의 복사 시작 위치, 복사를 당할 배열, 
복사를 당할 배열의 복사 시작 위치, 복사할 사이즈(인덱스의 갯수))*/
Buffer.BlockCopy(sourceArray, 0, destinationArray, 010);
 
Console.WriteLine();
 
// 복사한 후 복사를 당할 배열의 데이터를 출력해봅니다.
for (int i = 0; i < 10++i)
    Console.Write($"{destinationArray[i]} ");
// 출력 결과 : 0 1 2 3 4 5 6 7 8 9
 
Console.WriteLine();
 
cs


틀린 부분이 있을 수도 있습니다. 댓글로 알려주세요

+ Recent posts