static void Main(string[] args)
{
// 인터페이스 자체는 인스턴스화할 수 없지만 배열은 가능하다.
IDrawingObject[] instances = new IDrawingObject[] { new Line(), new Rectangle() };
foreach(IDrawingObject item in instances)
{
item.Draw(); // 인터페이스를 상속받은 객체의 Draw 메서드가 호출됨
}
// 자식 클래스로부터 암시적 형변환 가능
IDrawingObject instance = new Line();
instance.Draw();
}
클래스 간의 형변환
타입을 정의하는 것은 단위를 빈번하게 사용하는 프로그램에서도 유용하다.
대표적인 예로 통화(currency) 단위를 생각해 보자 원, 엔이 있을 때 이를 단순히
decimal 하나로 지정한다면 중요한 금전 계산에 오류가 발생할 여지가 남는다.
이런 경우 각 통화를 타입으로 정의하면
class Program
{
static void Main(string[] args)
{
Won won1 = new Won(1000);
Yen yen1 = new Yen(13);
yen1 = won1; // 타입이 다르기 때문에 컴파일 오류 발생
}
}
public class Currency
{
decimal money;
public decimal Money { get { return money; } }
public Currency(decimal money)
{
this.money = money;
}
}
public class Won : Currency
{
public Won(decimal money) : base(money) { }
public override string ToString()
{
return Money + "Won";
}
}
public class Yen : Currency
{
public Yen(decimal money) : base(money) { }
public override string ToString()
{
return Money + "Yen";
}
}
이처럼 컴파일 오류가 발생하기 때문에 부주의하게 통화를 섞어 쓰는 위험이 줄어든다.
하지만 환전을 해야하는 경우 통화 사이에 형 변환이 필요한데, 대입연산자(=)는 C#에서 재정의가 허용되지 않는다.
그래서 대체 구문으로 explicit, implicit 메서드를 정의할 수 있다.
public class Yen : Currency
{
public Yen(decimal money) : base(money) { }
public override string ToString()
{
return Money + "Yen";
}
static public implicit operator Won (Yen yen)
{
return new Won(yen.Money * 13); // 1엔당 13원으로 가정
}
}
class Program
{
static void Main(string[] args)
{
Yen yen1 = new Yen(100);
Won won1 = yen1; // 암시적 (inplicit) 형변환 가능
Won won2 = (Won)yen1; // 명시적 (explicit) 형변환 가능
Console.WriteLine(won1); // 출력: 1300Won
}
}
impicit operator를 오버로드했으므로 암시저 형변환을 할 수 있고, 암시적인 형뱐환이 가능하므로 명시적으로
캐스팅 연산자를 쓰는 것도 허용된다.
반드시 개발자가 의도한 형변환만 가능하도록 제한을 걸고 싶을 때는 implicit 대신 explicit 연산자를 사용한다.
public class Yen : Currency
{
public Yen(decimal money) : base(money) { }
public override string ToString()
{
return Money + "Yen";
}
static public explicit operator Won (Yen yen)
{
return new Won(yen.Money * 13); // 1엔당 13원으로 가정
}
}
class Program
{
static void Main(string[] args)
{
Yen yen1 = new Yen(100);
Won won1 = yen1; // 암시적 (inplicit) 형변환 불가능
Won won2 = (Won)yen1; // 명시적 (explicit) 형변환 가능
Console.WriteLine(won2); // 출력: 1300Won
}
}
반드시 형변환 연산자를 사용해야만 Won 타입으로 변경할 수 있다.
C#의 클래스 확장
중첩 클래스
클래스 내부에 클래스를 정의할 수 있다. 내부 클래스의 기본 접근 제한자는 internal이 아닌 private이 지정된다.
추상 클래스
부모 클래스의 인스턴스를 생성하지 못하게 하면서 특정 메서드에 대해 자식들을 반드시 재정의 하도록 강제할 때.
추상 클래스와 추상 메서드는 이런 상황을 위해 존재한다.
- 추상 메서드는 abstract 예약어가 지정되고 구현 코드가 없는 메서드이다.
일반 클래스에 존재할 수 없으며, 반드시 추상클래스 안에서만 선언할 수 있다. - 추상 클래스는 abstract 예약어가 지정돼 있다는 점을 제외하면 일반 클래스 정의와 동일하다.
new를 사용해 인스턴스를 만들 수 없고, 추상 메서드를 가질 수 있다는 차이가 있다.
class Point
{
int x, y;
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
public override string ToString()
{
return "X: " + x + ", Y: " + y;
}
}
abstract class DrawingObject
{
public abstract void Draw(); // 추상 메서드
public void Move() { Console.WriteLine("Move"); } // 일반 메서드도 정의 가능
}
class Line : DrawingObject // 추상 클래스를 상속받는 Line 클래스
{
Point pt1, pt2;
public Line(Point pt1, Point pt2)
{
this.pt1 = pt1;
this.pt2 = pt2;
}
public override void Draw() // 추상 클래스의 추상 메서드를 반드시 정의해야 함
{
Console.WriteLine("Line " + pt1.ToString() + " ~ " + pt2.ToString());
}
}
class Program
{
static void Main(string[] args)
{
DrawingObject line = new Line(new Point(10, 10), new Point(20, 20));
line.Draw(); // 다형성에 따라 Line.Draw가 호출됨
// Line X: 10, Y: 10 ~ X: 20, Y: 20
}
}
델리게이트
타입은 값을 담을 수 있다. 그렇다면 값의 범위에 메서드도 포함될 수 있을까?
short 형 변수가 short 값 범위의 값을 가리키는 것처럼 아래와 같은 Clean 메서드가 정의된 경우
public class Disk
{
public int Clean(object arg)
{
Console.WriteLine("직접 실행");
return 0;
}
}
-------------
Disk disk = new Disk();
[타입] cleanFunc = new [타입](disk.Clean); // 메서드를 인자로 갖는
// 타입의 인스턴스 생성
이렇게 메서드를 가리킬 수 있는 타입을 델리게이트라는 구문으로 제공한다.
일반적인 class 구문이 아닌 delegate라는 예약어로 표현한다.
접근 제한자 delegate 대상_메서드의_반환타입 식별자( 대상_메서드의_매개변수_목록);
대상이 될 메서드의 반환 타입 및 매개변수 목록과 일치하는 델리게이트 타입을 정의한다.
예를 들어 Clean 메서드에 대한 델리게이트 타입을 정의한다면

이렇게 해서 FuncDelegate라는 이름의 타입이 정의됐고, 이 타입은 int 반환값과 object 인자를
하나 받는 메서드를 가리킬 수 있다. 이를 이용해 앞의 코드를 다음과 같이 완성한다.
Disk disk = new Disk();
FuncDelegate cleanFunc = new FuncDelegate(disk.Clean); // 메서드를 인자로 갖는
// 타입의 인스턴스 생성
FuncDelegate cleanFunc = disk.Clean; // C# 2.0부터는 이 구문도 완전히 같은 역할을 한다.
이처럼 메서드를 가리키는 타입의 인스턴스인 cleanFunc라는 변수는 메서드를 가리키도 있으니 메서드를 호출하는 역할을 한다. 즉, 두 구문은 완전히 동일하게 실행된다.
Disk disk = new Disk();
FuncDelegate cleanFunc = disk.Clean;
disk.Clean(null); // Clean 메서드 직접 호출
cleanFunc(null); // 델리게이트 인스턴스를 통해 Clean 메서드 호출
인스턴스가 메서드를 호출할 수 있다는 점을 제외하고는 델리게이트는 완전히 타입에 속한다.
델리게이트를 담는 배열도 만들 수 있고, 시그니처가 동일한 메서드라면 인스턴스/정적 유형에 상관없이
모두 가리킬 수 있다. 다음은 이 모든 경우를 보여주는 또 다른 예이다.
public class Mathematics
{
delegate int CalcDelegate(int x, int y);
static int Add(int x, int y) { return x + y; }
static int Subtract(int x, int y) { return x - y; }
static int Multiply(int x, int y) { return x * y; }
static int Divide(int x, int y) { return x / y; }
CalcDelegate[] methods;
public Mathematics()
{
// static 메서드를 가리키는 델리게이트 배열 초기화
methods = new CalcDelegate[] { Mathematics.Add, Mathematics.Subtract,
Mathematics.Multiply, Mathematics.Divide };
}
// methods 배열에 담긴 델리게이트를 opCode 인자에 따라 호출
public void Calculate(char opCode, int operand1, int operand2)
{
switch (opCode)
{
case '+':
Console.WriteLine("+: " + methods[0](operand1, operand2));
break;
case '-':
Console.WriteLine("-: " + methods[1](operand1, operand2));
break;
case '*':
Console.WriteLine("*: " + methods[2](operand1, operand2));
break;
case '/':
Console.WriteLine("/: " + methods[3](operand1, operand2));
break;
}
}
}
class Program
{
// 3개의 매개변수를 받고 void를 반환하는 델리게이트 정의
// 매개변수의 타입이 중요할 뿐 매개변수의 이름은 임의로 정할 수 있음.
delegate void WorkDelegate(char arg1, int arg2, int arg3);
static void Main(string[] args)
{
Mathematics math = new Mathematics();
WorkDelegate work = math.Calculate;
work('+', 10, 5);
work('-', 10, 5);
work('*', 10, 5);
work('/', 10, 5);
}
}
---------출력 결과-----------
+: 15
-: 5
*: 50
/: 2
델리게이트가 타입이라는 점은 중요하다. 이 때문에 변수가 사용되는 곳이라면 델리게이트 또한 함께 사용되는데,
이것은 다음과 같은 의미를 갖는다.
- 메서드의 반환값으로 델리게이트를 사용할 수 있다.
- 메서드의 인자로 델리게이트를 전달할 수 있다.
- 클래스의 멤버로 델리게이트를 정의할 수 있다.
다시 한번 델리게이트가 메서드를 가리키는 것임을 떠올려보자. 따라서 다음과 같이 해석이 가능하다.
- 메서드의 반환값으로 메서드를 사용할 수 있다.
- 메서드의 인자로 메서드를 전달할 수 있다.
- 클래스의 멤버로 메서드를 정의할 수 있다.
이후 델리게이트의 특성을 좀 더 보강한 익명 함수, 람다 표현식이 제공된다.
델리게이트의 실체 : 타입
delegate 예약어는 메서드를 가리킬 수 있는 내부 닷넷 타입에 대한 "간편 표기법"이다. 그 내부 타입의 이름은 MulticastDelegate이다. delegate 예약어의 도움으로 MulticastDelegate의 존재를 모른 채 메서드를 가리키는 타입을 좀 더 쉽게 사용할 수 있었던 것이다.
MulticastDelegate의 이름에서 알 수 있듯이 사실 여러 개의 메서드를 가리키는 것도 가능하다.
아래는 2개의 정수에 대해 단 한 번의 함수 호출만으로 사칙 연산 메서드가 모두 호출되는 예제이다.
using System;
namespace ConsoleApp1
{
class Program
{
delegate void CalcDelegate(int x, int y);
static void Add(int x, int y) { Console.WriteLine( x + y ); }
static void Subtract(int x, int y) { Console.WriteLine( x - y ); }
static void Multiply(int x, int y) { Console.WriteLine( x * y ); }
static void Divide(int x, int y) { Console.WriteLine( x / y ); }
static void Main(string[] args)
{
CalcDelegate calc = Add;
calc += Subtract;
calc += Multiply;
calc += Divide;
calc(10,5);
}
}
}
------------출력 결과-------------
15
5
50
2
+= 연산자를 이용해 메서드를 델리게이트 인스턴스에 추가하는데, 이 역시 C# 컴파일러가 빌드 시에 자동으로
바꿔준다.
+=과는 반대 개념인 -= 연산자도 지원되는데 델리게이트에 사용하면 MulticastDelegate의 메서드 보관 목록에서
해당 메서드를 제거하는 역할을 한다.
델리게이트가 타입이라는 사실은 잊지말자. 클래스 내부에서 델리게이트를 정의했다면 그것은 중첩 클래스일 뿐
그 이상도 그 이하도 아니다.
콜백 메서드
사용자가 만든 Source타입에서 Target타입 내에 정의된 메서드를 호출한다고 하면 호출자는 Source가 되고 피호출자는 Target이 된다. 콜백이란 역으로 피호출자에서 호출자의 메서드를 호출하는 것이고,
이때 역으로 호출된 "호출자 측 메서드"를 콜백 메서드라고 한다.

res.sysnet.pe.kr/book/img/index.html
http://res.sysnet.pe.kr/book/img/index.html
res.sysnet.pe.kr
1번 호출에서는 Source 타입이 호출자이고 Target타입이 피호출자가 된다. 하지만 피호출자가 정의한 Do메서드 내부에서
다시 호출자의 타입에 정의된 메서드를 호출하고 있다. 2번 호출을 콜백이라 하고 Source 타입의 GetResult 멤버가 콜백 메서드가 된다.
콜백은 메서드를 호출하는 것이기 때문에 실제 필요한 것은 타입이 아니라 하나의 메서드일 뿐이다.
따라서 타입 자체를 전달해서 실수를 유발하기 보다는 메서드에 대한 델리게이트만 전달해서 이 문제를 해결할 수 있다.
델리게이트를 사용해 위 코드를 다시 구현해보자.
using System;
namespace Week1_5
{
delegate int GetResultDelegate(); // int를 반환하고 매개변수가 없는 델리게이트 타입 정의
class Target
{
public void Do(GetResultDelegate getResult)
{
Console.WriteLine(getResult()); // 콜백 메서드 호출
}
}
class Source
{
public int GetResult() // 콜백 용도로 전달될 메서드
{
return 10;
}
public void Test()
{
Target target = new Target();
target.Do(new GetResultDelegate(this.GetResult));
}
}
}
Target의 Do 메서드를 호출하면서 콜백 메서드를 전달했다.
이로 인해 Do 메서드는 내부의 동작에 콜백 메서드를 반영하게 된다. 이것은 이미 정의돼 있는 메서드내의
특정 코드 영역을 콜백 메서드에 정의된 코드로 치환하는 것과 같은 역할을 한다.
다음은 코드를 치환한다는 의미를 적절히 갈리는 예이다. 일반적인 선택 정렬 알고리즘이다.
using System;
namespace Week1_5
{
class SortObject // 배열을 정렬할 수 있는 기능을 가진 타입 정의
{
int[] numbers;
public SortObject(int[] numbers)
{
this.numbers = numbers; // 배열을 생성자의 인자로 받아서 보관
}
public void Sort() // 전형적인 선택 정렬 알고리즘을 구현한 메서드
{ // numbers 배열의 요소를 크기순으로 정렬
int temp;
for (int i = 0; i < numbers.Length; i++)
{
int lowPos = i;
for (int j = i + 1; j < numbers.Length; j++)
{
if (numbers[j] < numbers[lowPos])
{
lowPos = j;
}
}
temp = numbers[lowPos];
numbers[lowPos] = numbers[i];
numbers[i] = temp;
}
}
public void Display() // numbers 요소를 화면에 출력
{
foreach (int number in numbers)
{
Console.Write(number+", ");
}
}
}
class SortProgram
{
static void Main(string[] args)
{
int[] intArray = new int[] { 5, 2, 3, 1, 0, 4 };
SortObject sortObject = new SortObject(intArray);
sortObject.Sort();
sortObject.Display();
}
}
}
Sort라는 단 하나의 메서드를 제공해서 오름차순으로 정렬하고 있다. 여기서 내림차순으로 정렬하려면 중첩 for문 내
비교 연산자 하나만 수정하면 된다. 그럼 오름차순과 내림차순 정렬을 구현하려면 비교 연산자 하나 때문에 두 개의
메서드가 필요한데 이것은 코드가 중복되어서 바람직하지 않다.
물론, Sort 메서드에 bool ascending이라는 매개변수를 추가해 오름차순과 내림차순을 선택하게 하는 것도 좋다.
하지만 한번 더 생각해서 "비교하는 코드"를 외부에서 선택하도록 델리게이트로 만드는 것도 가능하다.
using System;
namespace Week1_5
{
class SortObject // 배열을 정렬할 수 있는 기능을 가진 타입 정의
{
//...생략
public delegate bool CompareDelegate(int arg1, int arg2);
public void Sort(CompareDelegate compareMethod)
{
//생략
for (int j = i + 1; j < numbers.Length; j++)
{
//...생략
if (compareMethod(numbers[j], numbers[lowPos]))
//...생략
}
class SortProgram
{
static void Main(string[] args)
{
int[] intArray = new int[] { 5, 2, 3, 1, 0, 4 };
SortObject sortObject = new SortObject(intArray);
sortObject.Sort(AscendingCompare);
sortObject.Display();
Console.WriteLine();
sortObject.Sort(DescendingCompare);
sortObject.Display();
}
public static bool DescendingCompare(int arg1, int arg2)
{
return (arg1 > arg2);
}
public static bool AscendingCompare(int arg1, int arg2)
{
return (arg1 < arg2);
}
}
}
단순한 예에서는 delegate보다 bool인자를 하나 더 받아서 해결할 수도 있지만 사용자 정보를 정렬한다고 가정했을 때,
주소, 나이, 이름 등 정렬기준이 복잡해지면 이를 처리하기 위한 인자와 코드가 정렬을 위한 코드보다 길어지고
복잡해진다. 항목이 추가되면 코드 수정도 어려워지기 때문에 delegate를 활용하는 것에 익숙해지자.
인터페이스
추상 메서드만 0개 이상 담고 있는 추상 클래스라고 생각해도 무방하다.
abstract class DrawingObject
{
public abstract void Draw();
public abstract void Move();
}
interface IDrawingObject
{
void Draw();
void Move();
}
그럼 왜 쓸까? 클래스는 다중 상속이 불가능하기 때문이다.
인터페이스의 메서드를 자식 클래스에서 구현할 때는 반드시 public 접근 제한자를 명시해야 한다.
아니면 다음과 같이 인터페이스명을 직접 붙이는 경우 public 접근 제한자를 생략해도 된다.
주의할 점은 public이 없다고 private이 되는 건 아니라는 점이다.
class notebook : Computer, IMonitor, IKeyboard
{
void IMonitor.TurnOn(){}
}
인터페이스는 메서드의 묶음이고, 프로퍼티도 내부적으로 메서드로 구현되기 때문에 인터페이스에는 프로퍼티 역시 포함될 수 있다.
interface IMonitor
{
void TurnOn();
int Inch { get; set; }
int Width { get; }
}
class Notebook : IMonitor
{
public void TurnOn();
int inch;
public int Inch
{
get { return inch; }
set { inch = value; }
}
int width;
public int Width { get { return width; } }
}
상속으로서의 인터페이스
인터페이스의 가장 기본적인 역할은 상속이다. 따라서 인터페이스를 구현한 것과 상속받았다는 것은 같은 의미를 가진다.
클래스 상속은 아니라서 구현 코드를 이어받은 것은 아니지만 적어도 메서드의 묶음에 대한 정의를 이어받은 것에 해당한다. 따라서 다른 클래스라도 인터페이스만 공통으로 구현되어 있다면 해당 구현 클래스의 인스턴스에 대해 인터페이스로 접근하는 것이 가능하다.
interface IDrawingObject
{
void Draw();
//void Move();
}
class Line : IDrawingObject
{
public void Draw() { Console.WriteLine("Line"); }
}
class Rectangle : IDrawingObject
{
public void Draw() { Console.WriteLine("Rectangle"); }
}
Line과 Rectangle은 IDrawingObject를 상속하고 있어 다음과 같은 활용이 가능하다.
static void Main(string[] args)
{
// 인터페이스 자체는 인스턴스화할 수 없지만 배열은 가능하다.
IDrawingObject[] instances = new IDrawingObject[] { new Line(), new Rectangle() };
foreach(IDrawingObject item in instances)
{
item.Draw(); // 인터페이스를 상속받은 객체의 Draw 메서드가 호출됨
}
// 자식 클래스로부터 암시적 형변환 가능
IDrawingObject instance = new Line();
instance.Draw();
}
인터페이스 자체로 의미 부여
인터페이스에 메서드가 포함돼 있지 않은 상태, 즉 비어있는 인터페이스를 상속받는 것으로도 의미가 부여될 수 있다.
예를 들어, System.Object 클래스의 ToString을 재정의한 클래스만 구분하고 싶다면 어떻게 해야 할까?
인터페이스를 활용하면 이렇게 구분하는 것이 가능하다.
interface IObjectToString {} // ToString이 재정의된 클래스에만 사용될 빈 인터페이스
class Computer {} // ToString을 재정의하지 않은 예제 타입
class Person : IObjectToString {}
private static void DisplayObject(object obj)
{
if ( obj is IObjectToString) // 인터페이스로 형변환이 가능하니?
{
Console.WriteLine(obj.ToString());
}
}
static void Main(string[] args)
{
DisplayObject(new Computer()); // 출력 안됨
DisplayObject(new Person("홍길동")); // Person: 홍길동
}
인터페이스가 계약이라고 정의했던 이유를 위의 예제를 통해 조금이나마 이해할 수 있을 것이다.
한마디로 인터페이스는 코드에서 자유롭게 정의할 수 있는 계약이다.
J & J - 정성태의 닷넷 이야기: Digital Stories
www.sysnet.pe.kr
https://ridibooks.com/books/1160000100
시작하세요! C# 10 프로그래밍
시작하세요! C# 10 프로그래밍 작품소개: 이 책의 목표는 여러분이 C#을 이용해 프로그래밍 기초를 탄탄하게 다질 수 있게 하는 것이다. 이를 위해 C# 언어의 최신 버전인 C# 10의 문법까지 구체적인
ridibooks.com
'C#' 카테고리의 다른 글
| 2023.09.13 BCL (시간, 문자열, StringBuilder, 정규 표현식, 컬렉션, ArrayList, Hashtable) (0) | 2023.09.13 |
|---|---|
| 2023.09.12 ref, out ,enum, readonly, const, event, index (0) | 2023.09.12 |
| 2023.09.11 인터페이스를 이용한 콜백 구현 ~ ref...ing (0) | 2023.09.11 |
| 2023.09.07 C# 정리 (0) | 2023.09.07 |
| 2023.09.06 C# 정리 (0) | 2023.09.06 |