지금까지는 구문들을 절차에 따라 작성하기만 했다면, 이제는 유지보수성을 증가시킬 방법에 대해 고민할 차례가 되었다.
전체 코드를 여러 파트로 나누고, 관리하는 방법에 대해 알아보자!
1) 전체 프로그램을 구성하는 코드를 여러 파트로 분리한 하나의 파츠를 의미
2) 각 모듈은 논리적 또는 기능적으로 분리되어 독립적인 일을 수행할 수 있어야 함
3) 모듈화는 경험치가 중요하기 때문에 숙련자를 통해 다양한 인사이트를 얻고 구현력을 높이는 것이 중요
4) 함수와 클래스: 프로그램의 모듈화를 위한 도구
1) 일련의 과정(기능)에 이름을 붙인 것
2) 서브루틴, 프로시저와 동의어
3) 객체지향 프로그래밍에서 메소드 또한 유사한 개념
4) 장점: 같은 기능을 수행하는 구문을 중복하여 작성하지 않아도 되므로 프로그램의 모듈성을 높일 수 있음
1) 함수 호출 시 함수 내부의 구문들이 모두 실행
2) 함수의 실행이 끝난 후 함수를 호출한 지점으로 돌아가 반환값을 줌
1) return-type identifier (parameter-list) { body }
int Max(int a, int b) // header
{
// body
int result = (a > b) ? a : b;
return result;
}
void Write(string message)
{
Console.WriteLine(message);
}
3) 다른 선언 방식
int Max(int a, int b) => (a > b) ? a : b;
1) 함수의 body에서 활용하는 변수
2) 함수를 호출할 때 알맞은 매개변수를 함께 넘겨줘야 함
3) 인자: 매개변수의 초기값을 의미 (즉, 실질적인 데이터)
1) 호출자(caller)
2) 피호출자(Calle)
3) return
1) 하나의 함수에서 매개변수의 개수와 타입을 다양하게 사용할 수 있도록 선언하는 방식
2) 가변인자 (Variadic Argument)
params
키워드를 사용하여 매개변수가 가변인자라는 것을 알림'어떻게 함수를 실행한 후 호출한 원래 위치로 실행 흐름이 돌아올 수 있는걸까?'
1) 함수의 동작에 필요한 모든 데이터를 저장한 공간 (Stack)
2) 정적 메모리 할당 방식
3) 스택 메모리 할당 방식
: 인수를 전달하는 규칙을 정한 것
1) 32bit 아키텍쳐를 주로 사용할 때는 호출규약이 다양했지만, 64bit 아키텍쳐를 많이 사용하게 되면서 호출규약이 통일됨
2) .Net Calling Convention
1) 지역변수
: 유효한 영역(블록) 내에서만 사용할 수 있는 변수
do
{
int i = 20;
} while(i = 20);
// 오류 -> { } 내에서만 유효하기 때문에 i를 조건으로 사용하려면 블록 밖에 선언해줘야 함
2) 전역변수
: 어디서든 사용할 수 있는 변수
: 객체가 유효한 시간
1) 지역변수의 수명
1) 인자 값의 복사본을 전달하는 방식
2) 원본에 영향을 주지 않음
3) 어떠한 키워드 등을 붙이지 않았을 때 기본적인 전달 방식은 Call By Value
// 값타입을 Call By Value 방식으로 전달한 예시
// 얕은 복사, 깊은 복사 개념이 적용되지 않는다
static void Main()
{
int num = 10;
Foo(num);
void Foo(int x) // num의 데이터 10으로 int x를 초기화
{
x = 20; // 원본 num에 영향 미치지 않음
}
}
// 참조타입을 Call By Value 방식으로 전달한 예시
// 얕은 복사가 일어난다
int[] arr = {1, 2, 3};
Boo(arr);
void Boo(int[] x) // arr에 저장된 실질적인 데이터인 [0]의 주소가 복사되어 전달
{
x[0] = 10; // 참조 타입은 주소가 전달되므로 원본 배열의 값에 영향을 미침
// [0]의 주소가 복사되어 전달되고, 배열의 작동 방식은 그 저장된 주소로부터
// 몇칸이 떨어졌는지 확인하여 접근하기 때문에 실제 원본의 값이 변경되는 것
}
1) 인자가 저장된 객체의 주소를 전달하는 방식
2) 주소를 통해 메모리 영역에 바로 접근할 수 있기에, 원본에 영향을 줄 수 있음
3) ref 키워드를 이용하여 인자의 주소값을 전달함을 명시
int num = 10;
Foo(num);
void Foo(ref int x)
{
x = 20; // ref를 이용해 주소를 전달했으므로, 원본인 num의 값이 변경됨
}
4) in 키워드
5) out 키워드
string CreateHuman(out int age) // 무조건적으로 반환할 데이터가 이거라는 걸 알리는 용도??
{
// 이름을 string으로 반환하고
// 나이를 int로 반환하고싶음
age = 35; // 이렇게 값을 넣어주지 않으면 함수 선언 자체에 컴파일 오류 발생
return "이름";
}
int hisAge;
string hisName = CreageHuman(out hisAge);
Console.WriteLine($"그의 이름은 {hisName}이며, 그의 나이는 {hisAge}입니다.");
6) in과 out을 이용해 코드의 의도를 명확하게 표현할 수 있으며, 이에 대해 제대로 처리하지 않으면 컴파일 오류가 발생하기에 실수를 예방할 수 있음
struct Something // 이 타입의 객체는 160 바이트
{
public int[] Numbers = new int[40];
public Something() { }
}
Something Sum2(Something a, Something b)
{
return a + b;
}
// 160바이트짜리 메모리를 두개 전달하려 함
// 레지스터의 크기는 8byte이기 때문에 불가
// ref로 주소를 전달하면 160byte의 데이터를 복사하여 레지스터에 저장할 필요 없기 때문에 가능
Something Sum2(ref Something a, ref Something b)
{
return a + b;
}
// good
// 근데 만약 의도치 않은 기능이 있다면?
Something Sum2(ref Something a, ref Something b)
{
a = new Something();
return a + b;
}
// 단순히 두 값을 더하는 의도로 사용하려했는데, 실수로 body의 첫줄과 같은 코드가 들어가는 경우
// a의 실제 값이 변경되기 때문에 의도와 다른 동작이 실행됨 (컴파일 오류가 안나서 발견하기 어려움)
Something Sume2(in Something a, ref Something b)
{
a = new Something(); // a는 in을 통해 수정이 불가하다고 명시했기 때문에 컴파일 오류 발생
return a + b;
}
// 이와 같이 in 키워드를 사용하면, 위와 같은 상황에 대해 컴파일 오류가 발생하기 때문에
// 값의 변경이 없다는 의도를 명확히하면서 실수를 방지할 수 있고
// 160byte 자체를 복사하지 않아도 되는 call by reference의 효과도 얻을 수 있음