오류나 예측 불가능한 상황을 방지하기위해 사용한다.
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
을 따라가보면 이 역시 하나의 클래스인 것을 확인할 수 있다.
(이하생략)
정적 배열 사용이 많은 C#의 경우, 배열 길이를 벗어난 참조에 대한 에러가 따로 있다.
당연히 Exception
클래스를 상속받는다
정확히는 SystemException
을 상속받고, 얘가 Exception
을 상속받는다.
catch (IndexOutOfRangeException e) {
Console.WriteLine(e.ToString());
}
이외에도 다양한 예외 클래스가 있지만, 나중에 사용할때 마다 찾아서 확인하는게 좋아보인다.
아니면 자주 사용하는 애들만 간추려보는것도...? 그때 가서 찾아보자
bool func(int x) {
return x >= 5 ? true : false;
}
함수 앞에 리턴값 타입을 적어준다. 만약 리턴값이 없다면 void
.
크게 js와 다른점은 없어보여 넘어가자!
여기서부터는 강의에 나오지않아 자체적으로 궁금한 점을 풀어봤다.
람다는 익명함수다
//기존 함수
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
는 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
가 필요한지 알았을 것이다. 함수의 인자로 함수를 넘겨주는, 주로 콜백패턴에서 자주 사용하는 행위를 하기 위해서다.
보통 이러한 행위를 고차함수(함수를 인자로 받거나 결과로 반환)라고 표현하기도 한다.
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버전에 추가되었다고 들었다.
결국 프로그래밍론은 서로 대척점에 있는 게 아니라 한 가지 방법론일 뿐, 적절한곳에 적절하게 사용하는 게 핵심이다!