1. 메서드(Method)
메서드에 관한 기본적인 내용은 아래 글에서도 다루고 있으니 참고 바랍니다.
[.NET/C#] - [C#] C#과 OOP(Object-Oriented Programming)
클래스(Class) 안에서는 메서드를 통해 특정 기능을 구현하게 되는데 종종 비슷한 기능을 인스턴스화 된 객체와 Type에서 각각 수행될 수 있도록 2중으로 구현하는 경우가 있습니다. 예를 들어 .NET string에 보면 Type에서 호출될 수 있는 Compare() 메서드와 인스턴스 객체에서 호출될 수 있는 CompareTo() 메서드가 있는데 이 둘은 비교를 수행한다는 점에서 비슷한 동작을 수행하며 해당 Type을 사용하는 개발자는 편의성을 고려해 이 둘의 메서드를 선택적으로 사용할 수 있게 됩니다.
Type에서 바로 호출될 수 있는 메서드인 경우에는 메서드를 static으로 생성하면 되며 그렇지 않고 인스턴스 객체에서 호출되는 메서드라면 static을 제외하여 메서드를 생성합니다.
namespace mylibrary;
public class Blick
{
public int BlockNumber;
public List<Blick> Blicks = new();
public static Blick Assembly(Blick b1, Blick b2)
{
Blick b = new() { BlockNumber = b1.BlockNumber + b2.BlockNumber };
b1.Blicks.Add(b);
b2.Blicks.Add(b);
return b;
}
public Blick AssemblyTo(Blick b)
{
return Assembly(this, b);
}
}
예제에서의 Blick 클래스는 다른 Blick클래스와 결합해 새로운 Blick을 생성할 수 있고 자기 자신은 그 새로운 Blick에 속할 수 있게 됩니다.
이를 위해 2개의 메서드를 가지고 있는데 하나는 Assembly()이고 다른 하나는 AssemblyTo()입니다. Assembly()를 보면 해당 메서드는 정적 메서드로 선언돼 Type에서 바로 호출이 가능한 상태이며 AssemblyTo()는 인스턴스 객체에서 호출되어 자기 자신과 다른 Blick 간 조립이 형성되는 기능을 수행합니다. 그런데 AssemblyTo()를 보면 메서드 내부에 새로운 로직이 구성되어 있는 것이 아니라 이미 위에서 선언한 Assembly() 메서드를 호출함으로써 코드의 중복 구현을 피하고 있습니다. 2개의 메서드를 만든다고 해서 각각에서 개별적인 로직 전체를 모두 구현할 필요가 없는 것입니다.
참고로 AssemblyTo()에서 나온 this는 자기 자신을 의미합니다.
using mylibrary;
Blick b1 = new() { BlockNumber = 1 };
Blick b2 = new() { BlockNumber = 2 };
var b3 = b1.AssemblyTo(b2);
Console.WriteLine(b3.BlockNumber);
var b4 = Blick.Assembly(b1, b2);
Console.WriteLine(b4.BlockNumber);
AssemblyTo()는 Assembly()를 호출하여 원하는 기능을 처리하고 있는데 Assembly()에서 반환하는 Blick은 값이 아닌 참 조형이므로 호출자가 결과를 받아 해당 객체에 대한 변경을 수행하면 객체가 존재하는 모든 영역에서 변경사항이 적용될 수 있습니다.
● 연산자 적용
위 예제에서 Assembly() 메서드는 Blick Type의 인수를 받아 BlockNumber값을 결합한 새로운 Blick를 반환하고 있습니다. 이를 위해 메서드는 다음과 같이 호출됩니다.
var b4 = Blick.Assembly(b1, b2);
그런데 이러한 방법보다 2개를 결합한다는 의미로 아래와 같이 할 수 있다면 이 방법이 더 자연스러울 수 있습니다.
var b4 = b1 + b2;
위의 구현을 가능하기 위해서는 메서드에 oprator와 필요한 연산자를 추가해 주면 됩니다.
public static Blick operator +(Blick b1, Blick b2)
{
Blick b = new() { BlockNumber = b1.BlockNumber + b2.BlockNumber };
b1.Blicks.Add(b);
b2.Blicks.Add(b);
return b;
}
연산자는 해당 연산에 필요한 문자(예제에서는 +)를 대신 사용할 뿐 메서드와 동일한 처리를 수행합니다. 다만 'operator 연산자'는 다른 언어의 컴파일러에서 지원되지 않는 경우가 생길 수 있으므로 같은 기능을 수행하는 메서드를 이름을 붙여 추가해 주는 것이 좋습니다.
public Blick Assembly(Blick b)
{
return this + b;
}
2. 이벤트(Event)
메서드는 객체가 수행하는 어떤 동작을 정의하는 반면 이벤트는 객체에서 발생하게 되는 특정 동작 자체로 설명될 수 있습니다. 가장 대표적으로 비유되는 사례가 사용자가 프로그램에서 특정 버튼을 클릭하는 등의 행위입니다.
● 델리게이트(delegates)
이벤트는 델리게이트에 의존합니다. 메서드를 호출할 때는 Type에 점(.)을 사용해 해당 Type이 가진 메서드를 호출하게 됩니다. 예를 들어 Console.WriteLine()는 Console Type에 있는 WriteLine() 메서드를 호출하는 것입니다. 이렇게 메서드를 호출하기 위한 다른 방법으로 델리게이트를 사용하는 방법이 있는데 델리게이트는 객체의 참조처럼 메서드를 저장하고 있는 메모리의 주소를 가진 것을 의미하며 메서드의 특징과 정확하게 일치하도록 되어 있으므로 델리게이트를 통해서도 메서드와 동일한 매개변수 형식을 가지고 메서드를 호출할 수 있습니다.
public class Car
{
public int Acceleration(string Gear)
{
if (Gear == "D")
return 80;
else
return 0;
}
}
예를 들어 위 예제의 경우 자동차 주행을 지시하기 위한 Acceleration()이라는 메서드가 있는데 메서드는 string형식의 매개변수를 받고 int형식의 값을 반환하고 있습니다. 따라서 다음과 같이 Acceleration()이라는 메서드를 호출할 수 있습니다.
Car sedan = new();
int speed = sedan.Acceleration("D");
이제 이와 동일한 동작을 델리게이트를 통해 구현하려면 우선 위 Acceleration()과 동일한 형식의 매개변수와 반환형 식이 일치하는 델리게이트를 아래와 같이 생성합니다.
delegate int DelegateAcceleration(string s);
메서드의 이름과 매개변수의 이름은 델리게이트를 생성하는데 전혀 고려대상이 아니지만 반환 형식과 매개변수 형식 자체는 본래 메서드의 형식과 정확히 일치해야 합니다.
그리고 위에서 선언한 델리게이트를 통해 메서드의 인스턴스를 다음과 같이 생성하고 생성된 델리게이트를 호출하면 메서드를 호출한 것과 정확히 같은 동작을 수행하는 걸 확인할 수 있습니다.
using mylibrary;
namespace myapp;
class Program
{
delegate int DelegateAcceleration(string s);
static void Main(string[] args)
{
Car c = new();
DelegateAcceleration d = new(c.Acceleration);
int speed = d("D");
Console.WriteLine(speed);
}
}
그렇다면 메서드가 아닌 델리게이트를 사용하는 이유는 무엇일까? 예를 들어 특정 메서드를 순서대로 호출하도록 하는 메서드의 Queue를 생성하거나 메서드를 병렬 실행하는 경우에도 델리게이트가 사용될 수 있습니다. 또한 델리게이트는 서로 다른 스레드에서 동작하면서 향상된 응답을 제공하기 위한 비동기 운용을 위해서도 사용될 수 있습니다.
무엇보다 가장 중요한 것은 델리게이트를 통해 이벤트를 구현함으로써 서로 다른 객체 간 메시지를 전달할 수 있다는 것입니다. 이로 인해 이벤트는 서로를 알지 못하는 여러 객체 사이에 느슨한 연결성을 제공할 수 있습니다.
마이크로소프트는 이벤트에 사용할 수 있는 사전에 정의된 2개의 델리게이트를 제공하고 있습니다.
public delegate void EventHandler(object? sender, EventArgs e);
public delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);
실제 여러분이 작성할 Type에서 이벤트를 구현하려면 위의 델리게이트 중 하나를 사용할 수 있습니다.
namespace mylibrary;
public class Car
{
public EventHandler? Deceleration;
public int speed;
public int Acceleration()
{
speed += 50;
if (speed > 100)
Deceleration?.Invoke(this, EventArgs.Empty);
return speed;
}
}
위 예제에서 Car는 가속을 의미하는 Acceleration() 메서드를 가지고 있으며 해당 메서드가 호출될 때마다 speed값을 50씩 증가시키게 됩니다. 단, speed가 100 값이 넘는 순간이 오면 위에서 정의된 EventHandler형식의 Deceleration로 Deceleration이벤트를 호출합니다.
이벤트를 호출하는 경우를 자세히 보면 Invoke() 메서드를 통해 이벤트를 발생시키고 있는데 이때 물음표(?)를 사용하여 Deceleration이 현재 null상태인지를 확인하고 있습니다. C# 6.0 이후부터 사용할 수 있는 방법이며 만약 null이라면 Invoke() 메서드를 호출하지 않을 것입니다. C# 6.0 이전 버전이라면 해당 코드는 아래와 같이 바뀔 수 있습니다.
if (speed > 100) {
if (Deceleration != null)
Deceleration(this, EventArgs.Empty);
}
이번에는 위에서 정의한 Car클래스를 사용하게 될 프로젝트에서 EventHandler와 동일한 형식의 이벤트 메서드를 다음과 같이 정의합니다. 참고로 마이크로소프트는 이벤트 메서드의 이름을 '객체명_이벤트명'으로 하기를 권장하고 있습니다.
static void Car_Deceleration(object? sender, EventArgs e)
{
if (sender is null)
return;
Car c = (Car)sender;
c.speed = 100;
Console.WriteLine($"과속방지 시스템 작동!! 현재 속도 {c.speed}");
}
이제 Car인스턴스 객체의 Deceleration을 통해 위에서 만든 이벤트를 할당하고 Acceleration() 메서드를 계속 호출해 봅니다.
static void Main(string[] args)
{
Car c = new();
c.Deceleration = Car_Deceleration;
c.Acceleration();
c.Acceleration();
c.Acceleration(); //과속방지 시스템 작동!! 현재 속도 100
}
전체 소스는 아래와 같습니다.
using mylibrary;
namespace myapp;
class Program
{
static void Main(string[] args)
{
Car c = new();
c.Deceleration = Car_Deceleration;
c.Acceleration();
c.Acceleration();
c.Acceleration(); //과속방지 시스템 작동!! 현재 속도 100
}
static void Car_Deceleration(object? sender, EventArgs e)
{
if (sender is null)
return;
Car c = (Car)sender;
c.speed = 100;
Console.WriteLine($"과속방지 시스템 작동!! 현재 속도 {c.speed}");
}
}
● 이벤트(Event)
위와 같은 예제에서는 이벤트로 구현해야 하는 메서드의 특징만 알고 있으면 메서드 자체를 델리게이트로 할당하여 이벤트를 발생시킬 수 있음을 알 수 있습니다.
하지만 이전 예제에서처럼 델리게이트로 이벤트를 할당할 때 굳이 할당 연산자(=)를 사용하지 않을 수도 있습니다. 델리게이트는 멀티캐스트로 이는 다수의 이벤트를 하나의 델리게이트 필드에 할당할 수 있음을 의미합니다. 이때는 기존에 사용했던 할당 연산자 대신 += 연산자를 사용해 다수의 이벤트를 할당합니다.
우선 델리게이트 필드의 선언 부분에 event keyword를 추가합니다.
public event EventHandler? Deceleration;
그리고 이벤트를 할당할 때 '='대신 '+='로 이벤트를 할당하도록 합니다.
Car c = new();
c.Deceleration += Car_Deceleration;
참고로 '-='연산자를 사용하면 할당된 이벤트를 델리게이트에서 제거함을 의미합니다.
소스코드를 수정한 후의 프로그램 동작 결과는 이전 예제와 완전히 동일합니다. 만약 할당해야 할 이벤트 메서드가 예제에서 처럼 단 하나뿐이라면 사실 굳이 event를 사용해야 할 필요는 없지만 그렇다고 하더라도 해당 델리게이트 필드가 이벤트를 할당받을 수 있음을 명시적으로 표현하기 위한 좋은 방법이 될 수 있으므로 event를 사용해 주기를 권장합니다.
'.NET > C#' 카테고리의 다른 글
[C#] 인터페이스(Interface)와 상속(Inheriting) - 3. 인터페이스(Interface) (0) | 2022.06.24 |
---|---|
[C#] 인터페이스(Interface)와 상속(Inheriting) - 2. 제네릭(generic) (0) | 2022.06.24 |
[C#] C#과 OOP(Object-Oriented Programming) - 7. 레코드(Record) (0) | 2022.06.24 |
[C#] C#과 OOP(Object-Oriented Programming) - 6. 패턴 매칭(Pattern Matching) (0) | 2022.06.24 |
[C#] C#과 OOP(Object-Oriented Programming) - 5. 속성(Property)과 인덱서(Indexer) (0) | 2022.06.24 |