C#을 배워보자 (4) - 예외처리, 함수, 람다와 클로져

김영현·2024년 11월 20일
0

C#

목록 보기
4/5

예외처리

오류나 예측 불가능한 상황을 방지하기위해 사용한다.
C#에서도 try-catch-finally로 예외를 처리함.

int[] arr = { 1, 2, 3 };

try {
	Console.WriteLine(arr[3]);
}
catch (Exception e) 
  { 
	Console.WriteLine(e.ToString());
  }
finally
  {
	Console.WriteLine("finally!");
  }

이렇게 catch에서 Exception e를 토하여 에러를 잡을 수 있음.
Exception을 따라가보면 이 역시 하나의 클래스인 것을 확인할 수 있다.


(이하생략)

IndexOutOfRangeException

정적 배열 사용이 많은 C#의 경우, 배열 길이를 벗어난 참조에 대한 에러가 따로 있다.
당연히 Exception클래스를 상속받는다

정확히는 SystemException을 상속받고, 얘가 Exception을 상속받는다.

catch (IndexOutOfRangeException e) { 
	Console.WriteLine(e.ToString());
}

이외에도 다양한 예외 클래스가 있지만, 나중에 사용할때 마다 찾아서 확인하는게 좋아보인다.
아니면 자주 사용하는 애들만 간추려보는것도...? 그때 가서 찾아보자


함수

bool func(int x) {
    return x >= 5 ? true : false;
}

함수 앞에 리턴값 타입을 적어준다. 만약 리턴값이 없다면 void.
크게 js와 다른점은 없어보여 넘어가자!


람다, delegate, 함수와 값

여기서부터는 강의에 나오지않아 자체적으로 궁금한 점을 풀어봤다.

람다는 익명함수다

//기존 함수
bool func(int x) {
	return x >= 5 ? true : false;
}

//람다
Func<int,bool> arrowFunc = x => x >= 5 ? true : false;

어디서 많이 본듯한 모양새다.

const func = (x:number) => x >= 5 ? 1 : 0

자바스크립트의 화살표 함수와 거의 똑같다.

그런데, C#에 화살표 함수(람다)는 왜 존재하는걸까? 단순하게 축약표현이 필요해서일까?
이 이야기를 하려면, 먼저 delegate에 대해 알아보아야 한다.

delegate

delegate는 C++의 함수 포인터 기능과 유사하다. 함수 포인터는 함수를 참조하는 것이다.
C언어를 다뤄보지는 않았지만, 함수포인터의 주 사용법은 콜백함수를 구현하기 위함이다.

#include <stdio.h>

void execute(int (*operation)(int, int), int x, int y) {
    printf("Result: %d\n", operation(x, y));
}

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

int main() {
    execute(add, 5, 7); // 출력: Result: 12
    return 0;
}

위에서 add라는 함수는 인자로써 execute함수에 전달되었다. 이때 *기호를 사용하여 함수포인터를 이용했다.

프로그래밍을 해 본 사람이라면, 콜백패턴은 주로 인자에 함수를 넘겨주어 처리한다고 알고있다.
그렇다면 함수는 어째서 바로 인자로 전달할 수 없을까?

함수와 값

C언어에서 변수는 을 저장한다.

int myNumber = 5;

myNumber라는 메모리공간을 나타내는 변수이름이 생기고, 해당 메모리에 5라는 값이 저장된다.
이 메모리 공간을 myNumber라는 변수를 통해 접근한다.

함수의 경우를 보자.

void myfunc() {
	printf("Hello, World!\n");
}

myfunc함수는 실행코드로 메모리 상의 코드 영역에 저장된다.
이때 myfunc는 별도의 값이 아닌, 함수가 메모리 상에 위치한 주소를 가진다.(5같은 특정한 값이 아님). 그리고 C에서는 함수 이름이 함수의 시작주소로 암묵적 변환된다.

#include<stdio.h> 
  
void funct() 
{ 
    printf("Hello, World!"); 
} 
  
int main(void) 
{ 
    printf("address of function main() is :%p\n", main); 
    printf("address of function funct() is : %p\n", funct); 
    return 0; 
} 

/*
address of function main() is :0x564f3255d168
address of function funct() is : 0x564f3255d149

[Execution complete with exit code 0]
*/

위에서 %p포인터 값(메모리 주소)를 출력하는 포맷 지정자다. 함수는 암묵적으로 메모리 상 주소를 가지기에 오류가 발생하지 않지만, 변수를 출력하게된다면, 오류가 발생한다.

int main(void) 
{ 
    printf("address of number my is :%p\n", my); 
    return 0; 
} 

/*
main.c: In function 'main':
main.c:8:39: warning: format '%p' expects argument of type 'void *', but argument 2 has type 'int' [-Wformat=]
    8 |     printf("address of number my is :%p\n", my);
      |                                      ~^     ~~
      |                                       |     |
      |                                       |     int
      |                                       void *
      |                                      %d
address of number my is :0x5

[Execution complete with exit code 0]
*/

포인터를 사용하면 오류가 나지 않는다.

int main(void) 
{ 
    printf("address of number my is :%p\n", (void*)&my); 
    return 0; 
} 

/*
address of number my is :0x55b491bfc010
*/

요약하자면 함수는 값이 아니다. 함수의 인자로는 값,참조만 올수 있다.
그렇기에 C#에서 delegate를 사용하여 함수의 인자로 함수를 넘겨주는 것이다.

delegate와 람다

여기까지 왔으면 왜 delegate가 필요한지 알았을 것이다. 함수의 인자로 함수를 넘겨주는, 주로 콜백패턴에서 자주 사용하는 행위를 하기 위해서다.

보통 이러한 행위를 고차함수(함수를 인자로 받거나 결과로 반환)라고 표현하기도 한다.

using System;

// 델리게이트 정의
public delegate int Operation(int a, int b);

public class Program
{
    // 고차 함수: 다른 함수(Operation)를 인자로 받음
    public static int ApplyOperation(int a, int b, Operation operation)
    {
        return operation(a, b);
    }

    // 더하기 함수
    public static int Add(int a, int b)
    {
        return a + b;
    }

    // 곱하기 함수
    public static int Multiply(int a, int b)
    {
        return a * b;
    }

    public static void Main()
    {
        // 델리게이트를 사용하여 함수를 전달
        Operation addOperation = Add;
        Operation multiplyOperation = Multiply;

        Console.WriteLine(ApplyOperation(3, 4, addOperation));  // 출력: 7
        Console.WriteLine(ApplyOperation(3, 4, multiplyOperation));  // 출력: 12
    }
}

고차함수를 만들기 위하여 delegate를 이용하여 메서드 타입을 명시적으로 정의한 뒤 사용하였다.
위 코드를 조금 더 간결하게 표현하면 아래와 같다.

using System;

public class Program
{
    // 고차 함수: 다른 함수(Func)를 인자로 받음
    public static int ApplyOperation(int a, int b, Func<int, int, int> operation)
    {
        return operation(a, b);
    }

    public static void Main()
    {
        // Func를 사용하여 함수를 전달
        Func<int, int, int> addOperation = (x, y) => x + y;
        Func<int, int, int> multiplyOperation = (x, y) => x * y;

        Console.WriteLine(ApplyOperation(3, 4, addOperation));  // 출력: 7
        Console.WriteLine(ApplyOperation(3, 4, multiplyOperation));  // 출력: 12
    }
}

따로 delegate를 정의하지 않았음에도 고차함수를 사용할 수 있게되었다.
이는 Func가 반환값이 존재하는 delegate를 추상화한 기능이기 때문이다.
Action이라는 키워드도 존재하는데, 이는 반환값이 없는 delegate만 적용이 가능하다.

위 함수를 더 간단히 만들 수 있다.

using System;

public class Program
{
    // 고차 함수: 다른 람다 표현식 (매개변수와 반환값을 갖는 함수)을 인자로 받음
    public static int ApplyOperation(int a, int b, Func<int, int, int> operation)
    {
        return operation(a, b);
    }

    public static void Main()
    {
        // 람다를 사용하여 함수를 정의
        Console.WriteLine(ApplyOperation(3, 4, (x, y) => x + y));  // 출력: 7
        Console.WriteLine(ApplyOperation(3, 4, (x, y) => x * y));  // 출력: 12
    }
}

즉석에서 만든 lambda(익명함수)다.

arr.map(() => {...})

마치 js에서 map등 고차함수를 사용할때 전달하게되는 익명함수와 똑같다.
가독성이 뛰어나고, 일회성 함수일 경우 따로 선언할 필요 없이 구현 가능하다.


클로져

js에서의 클로져는 외부함수, 내부함수의 렉시컬 환경 조화다.

const outerFunc = () => {
	let x = 1;
  	const innerFunc = () => ++x;
  	return innerFunc
}

const myFunc = outerFunc();

console.log(myFunc());
console.log(myFunc());
console.log(myFunc());

outerFunc의 실행은 myFunc에 할당되며 끝났지만, innerFunc가 변수 x를 참조중이기에 가비지컬렉터에 정리되지 않는다. 따라서 innerFunc에서 계속 접근 가능한 상태(기억중)로 남아 값이 갱신된다.

C#의 클로져도 굉장히 유사하다.

class Test{
	static void Main(string[] args){
    
		Func<Func<int>> outerFunc = () =>
		{
		int outerVar = 10;
		return new Func<int>(() => ++outerVar);
		};

		var myFunc = outerFunc();

		Console.WriteLine(myFunc());
		Console.WriteLine(myFunc());
        Console.WriteLine(myFunc());
    }
}

사실 클로져란게 js만의 개념은 아니기 때문이다.
1960년대 람다 표현식이 만들어진 이후 등장한 개념이기 때문!

그런데 C#을 배우다보니 한가지 의문이 생긴다.
js에서는 클래스를 이용한 private기능이 없었기에 보통 클로져(즉시실행함수)를 이용하여 상태를 캡슐,은닉화 했다.

하지만 객체지향기반인 C#에서는 prviate 접근 제한자가 있는데, 어째서 클로져가 필요했던 걸까?

이 부분은 로우 레벨 지식이 부족하여 아직 호기심으로만 남겨두겠다...🤔


느낀점

객체지향 언어라고 해서 무조건 클래스만 다루는게 아니었다. 언어가 발전하며 세대의 흐름을 흡수하는 것 같달까? 예를들어 lambda는 C#에도 있지만 자바도 8버전에 추가되었다고 들었다.
결국 프로그래밍론은 서로 대척점에 있는 게 아니라 한 가지 방법론일 뿐, 적절한곳에 적절하게 사용하는 게 핵심이다!

profile
모르는 것을 모른다고 하기

0개의 댓글