4. Method (메서드)
'함수'라고 하는 것과 개념이 동일하지만 어떤 Type(클래스)에서 하나의 Member로 취급될 수 있고 이러한 특징 때문에 이를 '메서드'라고 부르게 되었습니다.
● 값을 반환하기
메서드는 아무런 값도 반환하지 않거나 특정 값을 반환하도록 만들 수 있습니다. 그리고 값을 반환하는지에 대한 구분은 메서드에서 반환받을 값의 Type이 명시되었는지 혹은 void로 메서드가 수식되었는지로 판단할 수 있습니다.
namespace mylibrary;
public class Car
{
//void는 아무런 값도 반환하지 않음
public void Stop()
{
Console.WriteLine("자동차 정지");
}
//int형식의 값을 반환함
public int Drive()
{
Console.WriteLine("자동차 운행");
int currentSpeed = 50;
return currentSpeed;
}
}
using mylibrary;
Car sedan = new();
Car truck = new();
sedan.Stop();
truck.Drive();
//자동차 정지
//자동차 운행
● tuple
Method는 값을 반환할 때 return 구문을 사용하는데 이때 반환 대상은 단 하나의 객체만을 대상으로 할 수 있습니다. 하지만 메서드를 실행하고 난 뒤 여러 값에 대한 확인이 필요한 경우가 있는데, tuple이전에는 주로 반환에 필요한 새로운 Type을 생성하는 방법으로 이를 해결했다면
namespace mylibrary;
public class Car
{
//void는 아무런 값도 반환하지 않음
public void Stop()
{
Console.WriteLine("자동차 정지");
}
//int형식의 값을 반환함
public CarState Drive()
{
Console.WriteLine("자동차 운행");
return new CarState()
{
Speed = 80,
RPM = 2500
};
}
}
public class CarState
{
public int Speed;
public int RPM;
}
Tuple을 사용하면 좀 더 간결한 방법으로 필요한 값을 전달할 수 있습니다.
public (int, int) Drive()
{
Console.WriteLine("자동차 운행");
return (80, 2500);
}
괄호를 통해 Tuple을 사용하는 방식은 2017년 C# 7.0에서 지원되기 시작한 것으로 .NET에서는 이를 위해 새로운 System.ValueTuple Type을 추가하게 되었습니다.
using mylibrary;
Car sedan = new();
Car truck = new();
sedan.Stop();
(int, int) Driving = truck.Drive();
Console.WriteLine($"현재 속도 : {Driving.Item1}, 현재 RPM : {Driving.Item2}");
//자동차 정지
//자동차 운행
//현재 속도 : 80, 현재 RPM : 2500
위 예제를 보면 어떤 것이 속도이고 어떤 것이 RPM인지는 반환되는 값으로만 판단할 수 있고 그나마 각각의 값이 무엇인지는 제대로 표현되지 않았습니다. 또한 값을 전달받은 상태에서는 단순히 Item1, Item2등으로만 접근할 수 있는데 이러한 경우는 명시적으로 직접 명칭을 부여하여 해결할 수 있습니다.
public (int Speed, int RPM) Drive()
{
Console.WriteLine("자동차 운행");
return (Speed : 80, RPM : 2500);
}
그리고 이렇게 정의된 메서드는 var 키워드를 통해 혹은
(int speed, int RPM) Driving = truck.Drive();
처럼 '튜플 분해'방식을 통해 그 결괏값을 전달받아야 합니다.
var Driving = truck.Drive();
Console.WriteLine($"현재 속도 : {Driving.Speed}, 현재 RPM : {Driving.RPM}");
명시적으로 Tuple에 Name이 결정되어 있는 것을 받으면 해당 Name을 그대로 사용할 수 있게 되는데 이는 C# 7.1에서 추가된 '튜플 이름 추론'으로 가능합니다. 즉, 명명된 이름이 존재하지 않는 경우 Item1...Item2같은 방식으로 사용되지만 명시적인 이름이 사용되는 경우 지정한 이름을 그대로 사용할 수 있게 되는 것입니다.
참고로 tuple뿐만이 아니라 Type 또한 분해가 가능합니다. 모든 Type(Class)는 Deconstruct()라는 특별한 Method를 가질 수 있는데 이 Method는 Type을 각각의 객체로 분해할 수 있도록 합니다.
namespace mylibrary;
public class Car
{
int currentSpeed;
int currentRPM;
public void Drive(int Speed)
{
currentSpeed = Speed;
//RPM은 속도의 2배가 된다고 가정함
currentRPM = (int)currentSpeed * 2;
}
public void Deconstruct(out int speed, out int rpm)
{
speed = currentSpeed;
rpm = currentRPM;
}
}
예제에서 작성된 Deconstruct() 메서드는 Type을 분해하길 원하는 형식으로 대입하면 자동으로 실행되어 지정한 값을 전달하게 됩니다.
using mylibrary;
Car sedan = new();
sedan.Drive(80);
var (speed, rpm) = sedan;
Console.WriteLine($"현재 차량의 속도는 {speed}이며 RPM은 {rpm}입니다.");
//현재 차량의 속도는 80이며 RPM은 160입니다.
● 매개변수(parameter)의 정의와 인수의 전달
매개변수(parameter)는 이전에서 봐온 것처럼 Method를 정의할 때 괄호 안에서 필요한 변수가 정의된 것을 말합니다. 가끔 '인수'라는 말도 사용하는데 인수는 매개변수 안에 존재하는 값 자체를 의미합니다.
public void Drive()
{
currentSpeed = 60;
currentRPM = 120;
}
public void Drive(int Speed)
{
currentSpeed = Speed;
//RPM은 속도의 2배가 된다고 가정함
currentRPM = (int)currentSpeed * 2;
}
따라서 예제에서 매개변수는 Speed에 해당하며 메서드를
Drive(80);
처럼 호출하는 경우 인수는 '80'에 해당합니다.
● 메서드 오버 로딩(Method Overloading)
이전 예제는 매개변수를 가지는 Drive(int Speed) 메서드와 매개변수가 없는 Drive() 메서드 2가지가 존재합니다. 즉, 특별한 값이 전달되지 않는 경우의 메서드가 마련된 셈인데 매개변수의 상황에 따라 예제에서 처럼 동일한 메서드를 생성하는 것을 '메서드 오버 로딩'이라고 합니다.
이러한 방식은 메서드를 사용하는 사용자 입장에서는 같은 메서드를 Drive()로 호출하거나 Drive(80)처럼 매개변수의 변동사항만 적용하여 호출할 수 있게 합니다. 다만 메서드 오버 로딩은 대상이 되는 메서드의 반환형 식이 같아야 합니다. 예제에서는 2개의 Drive() 메서드 모두 void로 수식되어 있습니다.
● 명명된 매개변수와 선택적 전달
메서드를 간소화하는 방법 중 하나로 매개변수를 optional로 지정하는 것이 있습니다. 이 방법은 매개변수의 일부나 전체에 대해 기본값을 할당하는 것입니다. 다만 optional은 다수의 매개변수에서 마지막에 와야 합니다. optional매개변수 다음에 비 optional매개변수를 정의할 수 없습니다.
namespace mylibrary;
public class Car
{
int currentSpeed;
int currentRPM;
char gear;
public void Drive(int speed = 80, char gear = 'D')
{
currentSpeed = speed;
//RPM은 속도의 2배가 된다고 가정함
currentRPM = (int)currentSpeed * 2;
this.gear = gear;
}
}
예제에서 Drive() 메서드는 speed와 gear2개의 매개변수를 갖는데 모두 기본값이 지정되어 있습니다. 이 상태에서 아래와 같이 메서드를 호출하면
using mylibrary;
Car sedan = new();
sedan.Drive();
//80의 속도로 D 주행 중...
인수가 지정되지 않은 모든 매개변수는 자신에게 할당된 기본값을 대신 사용하게 됩니다. 물론 기존과 같이 원하는 값을 전달하는 것도 가능합니다.
Car sedan = new();
sedan.Drive(80);
//80의 속도로 D 주행 중...
sedan.Drive(10, 'R');
//10의 속도로 R 주행 중...
위에서 처럼 인수를 지정하면 각각의 값은 메서드에 정의된 순서대로 매개변수에 전달됩니다. 그러나 매개변수가 너무 많은 경우 이를 기억하고 적절한 값을 전달하는 게 쉽지 않을 수 있는데 그런 경우에는 직접 매개변수명을 지정해 필요한 값을 전달할 수 있습니다. 이를 '명명된 매개변수'라고 합니다.
Car sedan = new();
sedan.Drive(speed: 80);
//80의 속도로 D 주행 중...
sedan.Drive(gear: 'R', speed: 10);
//10의 속도로 R 주행 중...
명명된 매개변수를 사용하는 경우 위 예제에서 처럼 메서드에 정의된 매개변수의 순서를 바꿀 수도 있습니다. 다만 '명명된 매개변수'를 일부 매개변수에만 적용하는 경우
sedan.Drive(speed: 10, 'R');
//10의 속도로 R 주행 중...
에는 매개변수의 순서와 일치해야 합니다.
● ref와 out
메서드를 호출할 때 전달하는 인수는 기본적으로 값에 의한 전달이 이루어집니다. 내가 전달한 값이 매개변수에 그대로 전달되고 해당 매개변수는 매개변수가 정의된 메서드 안에서 사용될 것입니다.
그러나 때로는 매개변수의 값을 해당 메서드의 외부에서도 접근해야 하는 경우가 있는데 이런 경우에는 ref혹은 out으로 매개변수를 지정할 수 있습니다.
namespace mylibrary;
public class Car
{
public void Drive(int speed, ref int rpm, out int cc)
{
cc = 0;
speed++;
rpm++;
cc++;
}
}
위 예제에서 Drive() 메서드는 총 3개의 매개변수가 지정되어 있습니다. rpm은 ref로 cc는 out으로 수식되어 있는데 다만 cc매개변수의 경우 밑에서 자세히 설명하겠지만 일단 out으로 된 매개변수는 out의 특성상 기본값을 가질 수 없고 메서드 안에서 반드시 초기화가 이루어져야 합니다.
using mylibrary;
Car sedan = new();
int s = 80;
int r = 2000;
int c = 999;
Console.WriteLine($"{s}-{r}-{c}");
sedan.Drive(s, ref r, out c);
Console.WriteLine($"{s}-{r}-{c}");
//80-2000-999
//80-2001-1
예제는 위에서 정의된 Drive() 메서드를 호출하는데 ref는 ref로 out은 out으로 지정되어 호출됨을 알 수 있습니다.
결과를 보면 일반적인 매개변수의 경우는 값 형식으로 값을 전달하는 방식이며 값이 그대로 매개변수에 복사되므로 메서드 안에서의 매개 변숫값과 메서드를 호출할 때의 값은 별개로 유지됩니다.
하지만 ref는 값이 아닌 참조를 전달합니다. 예제에서는 r변수를 메서드의 rpm으로 전달하고 있는데 이때는 참조를 전달하는 것이므로 rpm은 r변수를 참조하게 됩니다. 따라서 메서드 내부에서 rpm값이 바뀌면 r변수에도 그대로 적용되는 것입니다.
out매개변수도 ref와 마찬가지로 참조를 전달하지만 역으로 메서드에 있는 매개변수의 것을 참조하게 되므로 변수 c는 매개변수 cc를 참조하게 됩니다. 그래서 메서드 안에서 매개변수의 값이 바뀌면 그대로 변수c의 값에도 영향을 미치게 됩니다. 이러한 out의 특징 때문에 out은 메서드안에서 반드시 초기화나 값의 재할당이 이루어져야 합니다.
out매개변수는 어차피 메서드안에서 새로운 값이 할당될 것이므로 사실 위의 예제에서 변수 c를 cc에 전달하는 것 자체가 무의 합니다. 이러한 특징 때문에 C# 7.0부터는 out 매개변수는 메서드를 호출할 때 다음과 같이 out매개변수를 위한 변수의 선언을 생략할 수 있게 되었습니다.
using mylibrary;
Car sedan = new();
int s = 80;
int r = 2000;
Console.WriteLine($"{s}-{r}");
sedan.Drive(s, ref r, out int c);
Console.WriteLine($"{s}-{r}-{c}");
● partial
OOP에서 특히 C#에서 클래스는 경우에 따라 수십 가지 Field와 Member를 가질 수 있습니다. 경험에 의하면 1만 9천 라인이 넘어가는 Class를 접할 때도 있었는데 Class가 이렇게 비대해지는 경우 관리상의 어려움이 발생할 수 있습니다. 이런 경우 하나의 큰 Class를 partial을 통해 여러 부분으로 나누어 각각의 파일로 저장하게 되면 훨씬 관리가 용이해질 수 있습니다.
namespace mylibrary;
public partial class Car
{
int currentSpeed = 0;
char currentGear;
public void Drive(int speed, char gear)
{
currentSpeed = speed;
currentGear = gear;
}
}
public partial class Car
{
public void Stop()
{
currentSpeed = 0;
currentGear = 'P';
}
}
예제에서 Car클래스는 partial로 분리되었으며 각각의 Class는 하나의 Car클래스와 동일하므로 Field에 접근하는 것처럼 아무런 문제 없이 Class가 작성될 수 있습니다.
예제에서는 partial을 하나의 파일 안에서 나누었는데 대부분은 별도의 파일을 통해 나누어져 관리하게 됩니다.