p221~
깊은 복사와 얕은 복사
구조체 a = 구조체 b 할당 시 구조체는 인스턴스가 가진 메모리 자체가 복사되어 새로운 변수에 대입되는데 이를 깊은 복사(deep copy)라 한다.
참조형식의 변수가 대입되는 방식을 얕은 복사(shallow copy)라 한다.
Vector v1;
v1.x = 5;
v1.y = 10;
Vector v2 = v1;
v2.x = 7;
v2.y = 14;
Console.WriteLine("v1: X= " + v1.x + ", Y = "+ v1.y);
Console.WriteLine("v2: X= " + v2.x + ", Y = " + v2.y);
//출력결과
// v1: X= 5, Y = 10
// v2: X = 7, Y = 14
struct Vector
{
public int x;
public int y;
}
깊은 복사는 메모리 자체를 복사하여 v2가 값이 변경되면 v1에 영향을 주지 않는다.
Vector v1 = new();
v1.x = 5;
v1.y = 10;
Vector v2 = v1; // 참조형식의 대입은 참조 주소만 복사됨
v2.x = 7;
v2.y = 14;
Console.WriteLine("v1: X= " + v1.x + ", Y = "+ v1.y);
Console.WriteLine("v2: X= " + v2.x + ", Y = " + v2.y);
//출력결과
// v1: X= 7, Y = 14
// v2: X = 7, Y = 14
class Vector
{
public int x;
public int y;
}
얕은 복사는 v2가 v1이 참조하는 주소를 가리키게 된다. v2가 값을 변경 시 v1이 값 변경되고 v1이 값 변경시 v2도 동일한 값으로 변경된다.
이는 메서드의 매개변수를 값 형식 또는 참조형식일때도 위와 동일하다.
값 형식의 인스턴스는 메서드로 전달될 때 복제되어 또 다른 인스턴스가 생성되고 해당 인스턴스를 새롭게 정의한 변수가 가리킨다. 따라서 특정 메서드 내에서 매개변수의 값 형태의 변수 값을 변경하는 것은 원래의 인자 변수에 영향을 미치지 않는다.
반면 참조 변수는 인자의 변수와 매개변수 둘다 동일한 인스턴스를 가리킨다. 즉 메서드에 넘겨지는 인자 변수가 가진 참조 주소를 가리킨다. 이로인해 메서드에서 값을 변경 시 그 영향이 메서드를 호출한 측의 참조변수(인자)에도 미친다.
깊은 복사의 장점은 값의 변경에 대한 간섭없이 개발자가 원하는 동작을 한다는 것이지만 구조체가 내부에 많은 필드를 담게 되어 크기가 1024바이트까지 커지면 해당 구조체 변수를 메서드에 전달 할 때마다 컴퓨터는 1kb의 메모리 영역을 매번 복사하는 작업을 해야한다. 반면 참조타입인 클래스로 정의했다면 메서드를 호출할 때마다 참조 주솟값만 복사하면 되므로 구조체와 비교해 월등한 성능향상이 될 수 있다.
구조체와 클래스 선택 기준.
1. 일반적으로 모든 사용자 정의 타입은 클래스로 구현.
2. 깊은/얕은 복사의 차이가 민감한 타입은 선택적으로 구조체로 구현.
3. 참조형식은 GC(가비지 수집기)에 의해 관리받는다. 따라서 참조형식을 사용 시 GC에 부담이 되는데 이런 부하를 피해야하는 경우 구조체를 선택한다.
ref 예약어
얕은 복사와 깊은 복사의 동작 방식에 공통점으로는 변수의 스택 값은 여전히 복사된다는 점이다.
값 형식의 변수는 해당 변수가 실제 값을 가리키고 있고, 따라서 그 값이 복사되어 전달된다. 반면 참조 형식의 변수는 힙에 존재하는 실제 데이터의 주솟값을 가리키고 있으며 따라서 그 주솟값이 복사되어 전달된다.
'변수의 스택 값' 이 복사되는 상황을 메서드의 인자전달과 관련해 '값에 의한 호출'(Call by value)이라 한다.
'참조에 의한 호출'(Call by reference)는 메서드에 인자를 전달하면 변수의 스택 값이 복사되는 것이 아니라 해당 변수의 스택 값을 담고 있는 주소 자체가 전달된다. c#에서는 참조에 의한 호출을 지원하기 위한 ref, out 두가지 예약어가 있다.
ref 예약어 사용 법
1) 메서드의 매개변수를 선언할 때 함께 표기.
2) 해당 메서드를 호출하는 측에서도 명시해야한다.
using System.Numerics;
Vector v1;
v1.X = 5;
v1.Y = 10;
Change(ref v1); // 메서드 호출 시 ref 예약어 사용
Console.WriteLine($"X: {v1.X} , Y: {v1.Y}".ToString());
void Change(ref Vector vt)
{
vt.X = 7;
vt.Y = 14;
}
struct Vector
{
public int X;
public int Y;
}
#출력 결과
X: 7, Y: 14
* C# 7.0에서 ref 예약어를 지역변수와 반환값에도 적용 가능(12.2절 참고)
** C# 7.2에서 in 예약어 추가(14.2절 참고)
ref 예약어는 구조체를 클래스처럼 '얕은 복사'로 전달한 것과 동일 효과를 내고 있지만 얕은 복사와 ref 예약어는 동작 방식에 차이가 있다.
ref를 사용 하지 않았을때는 값 형식의 스택 복사가 있었지만, ref를 사용하게 되면 메서드의 vt 변수가 호출 측의 v1변수와 동일한 주소를 가리키게 된다.
ref 예약어는 참조형 변수에도 사용 가능하다.
4.13 과 비교하면 참조형 변수가 ref 예약어로 인해 동작 방식이 달라져있다.
class Point
{
public int x;
public int y;
}
class Program
{
static void Main(string[] args)
{
Point pt1 = new Point();
pt1.x = 5;
pt1.y = 10;
Change(ref pt1);
Console.WriteLine($"{pt1.x} , {pt1.y}");
}
private static void Change(ref Point pt)
{
pt.x = 7;
pt.y = 14;
}
}
다음의 실행 결과는 원래의 참조형 변수가 얕은 복사로 전달된 것과 동일한 결과를 보여준다. (x: 7, y: 14)
ref 사용안한 얕은 복사는 다음과 같은 주소다.
참조형 변수를 ref 예약어로 전달한 효과를 구분하기 위한 특수한 예제는 다음과 같다.
class Point
{
public int x; public int y;
}
class Program
{
static void Main(string[] args)
{
Point pt1 = null;
Change1(pt1); // 메서드 호출 : 얕은 복사
Console.WriteLine("pt1: " +pt1);
Change2(ref pt1); // ref 사용
Console.WriteLine("pt1: X = " + pt1.x + ", Y = " + pt1.y);
}
private static void Change2(ref Point? pt) // ref를 이용한 참조에 의한 호출
{
pt = new Point();
pt.x = 7;
pt.y = 14;
}
private static void Change1(Point? pt) // 얕은 복사
{
pt = new();
pt.x = 6;
pt.y = 12;
}
}
#출력
pt1:
pt1: X = 6, Y = 12
P230 (중요)
그림 4.16 얕은 복사로 전달된 참조값
그림 4.17 ref 예약어로 같은 주소를 가리키는 변수
구조체, 클래스 뿐만 아니라 기본 자료형에도 참조에 의한 호출을 사용 할 수 있다.
int value1 = 5;
int value2 = 10;
SwapValue(ref value1, ref value2);
Console.WriteLine($"{value1}, {value2}") ;
void SwapValue(ref int value1, ref int value2)
{
int tmp = value1;
value1 = value2;
value2 = tmp;
}
#출력
10, 5
메서드에 ref 인자로 전달되는 변수는 호출 하는 측에서 반드시 값을 할당해야만 한다.
int value1; // 값이 없으므로 ref 인자로 전달 불가
string txt = null; // null 값 가지므로 ref 인자로 전달 가능
int value2;
value2 = 5; // 메서드 호출 전에 값을 가지면 ref 인자로 전달 가능
vector vt;
vt.x = 5; // x, y가 포함된 vector 구조체에 y값 초기화하지 않았을 경우 ref 인자로 부적절.
vector vt2 = new(); // x, y 필드가 0으로 초기화됐으므로 ref 인자로 전달 가능
out 예약어
참조에 의한 호출을 가능하게 하는 또 하나의 예약어는 out이다.
ref와의 차이점.
1. out으로 지정된 인자에 넘길 변수는 초기화되지 않아도 된다. 초기화돼 있더라도 out 인자를 받는 메서드에는 그 값을 사용 할 수 없다.
2. out으로 지정된 인자를 받는 메서드는 반드시 변수에 값을 넣어서 반환해야 한다. (그렇지 않을 경우 컴파일 에러 발생)
out 예약어가 사용되는 곳에 ref 예약어를 사용해 구현이 가능하다. out 예약어는 ref 기능 가운데 몇 가지를 강제로 제한하여 개발자가 특별한 용도로 사용하게 끔 제공한 것이다.
out 예약어의 용도.
메서드는 단 1개의 반환값만 가질 수 있지만, out으로 지정된 매개변수를 사용함으로써 여러 개의 반환 값을 반환할 수 있다.
나눗셈 연산에서 분모가 0인 경우를 제한하는 코드이다.
DivideResult Divide(int n1, int n2)
{
DivideResult ret = new();
if(n2 == 0)
{
ret.Success = false;
return ret;
}
ret.Success = true;
ret.Result = n1 / n2;
return ret;
}
struct DivideResult
{
public bool Success;
public int Result;
}
out예약어를 사용해보자.
int quotient;
if(Divide(15, 3, out quotient) == true)
{
Console.WriteLine(quotient);
}
bool Divide(int n1, int n2, out int result)
{
if(n2 == 0)
{
result = 0;
return false;
}
result = n1 / n2;
return true;
}
코드가 간결해짐을 확인 할 수 있다.
out으로 지정된 result 변수는 메서드가 return 하기 전에 반드시 초기화돼 있어야 한다. 그렇지 않으면 컴파일 에러가 발생된다.
// System.Int32 타입에 정의된 TryParse 정적 메서드
public static bool TryParse(string s, out int result);
닷넷에서는 각 기본 타입에 TryParse 메서드를 제공한다.
이 메서드는 변환 성공 여부를 true/false로 반환하고, 변환 성공 시 out으로 지정된 result 변수에 값을 반환한다.
int n;
if(int.TryParse("12345", out n) == true)
{
Console.WriteLine(n); // 출력: 1234
}
double d;
if(double.TryParse("123E3", out d) == true) // double은 지수 표기법의 문자열도 지원. int는 지원 x
{
Console.WriteLine(d); //출력 결과 123000
}
bool b;
if(bool.TryParse("true", out b) == true)
{
Console.WriteLine(b); // 출력 결과 True
}
short s;
if(short.TryParse("1231235125", out s) ==true)//false 반환 (short 범위를 초과했기에)
{
Console.WriteLine(s); // false 반환됐으므로 실행되지 ㅇ낳음
}
if(short.TryParse("Not_a num", out s) == true) // 숫자가 아니므로 false
{
Console.WriteLine(s); // false 반환됐으므로 실행되지 않음
}
int n = 500;
string txt = n.ToString(); // int -> 문자열
int ret;
int.TryParse(txt, out ret); // 문자열->int 형 값 복원
#해당 원리를 이용해 타입의 값을 프로그램 외부에 저장하고 복원 할 수 있다.
ref와 out의 특성을 IN, OUT이라는 표현으로 간단히 나타내기도 한다.. ref는 값을 메서드 측에 전달(IN)하고 전달 받기(OUT)도 하기에 [IN, OUT] 특성이 있으며, out은 메서드 측으로부터 전달받기(OUT)만 가능하므로 [OUT] 특성을 띤다.
4.5.1.6 열거형
Days today = Days.SUNDAY;
Console.WriteLine(today);
enum Days { SUNDAY, MONDAY, TUESDAY}
# 출력 결과 : SUNDAY
enum의 출력 값이 0(System.Int32 타입의 값)이 아닌 SUNDAY(문자열)로 나온 이유는?
enum의 조상이 System.Object임을 감안해 enum은 ToString 메서드를 재정의 했고, 그것의 내부 코드에서 숫자값보다는 문자열 로 반환하는 역할을 하기 때문이다.
enum은 System.Int32를 부모로 두기에 각종 숫자형 타입과 형 변환하는 것이 가능하고 그 반대도 마찬가지다.
제약이라면 암시적 형 변환이 아닌 명시적 형 변환을 해야한다.
Days today = Days.SUNDAY;
int n = (int)today;
short s = (short)today;
Console.WriteLine(n); //enum ->int 명시적 변환
Console.WriteLine(s); // enum -> short 명시적 변환
today = (Days)s; // 숫자 -> enum 형식으로 명시적 변환
Console.WriteLine(today); //출력 SUNDAY
enum Days { SUNDAY, MONDAY, TUESDAY}
HasFlag 메서드를 사용한 특정 요소값 포함여부
예제 4.27 다중 값을 포함하는 enum 인스턴스
Days workingDays = Days.MONDAY | Days.TUESDAY | Days.THURSDAY;
Console.WriteLine(workingDays.HasFlag(Days.SATURDAY)); //SUNDAY를 포함하고 있나?
//출력이 False가 아닌 True로 나온다.
Days today = Days.MONDAY;
Console.WriteLine(workingDays.HasFlag(today)); // 출력 : True
Console.WriteLine(workingDays); // 출력: 7
enum Days
{
SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,SATURDAY
};
코드에서 HasFlag(Days.SATURDAY)의 결과가 True로 나오는 이유는 Days 열거형이 [Flags] 속성을 갖지 않고, 기본적으로 정수형 값을 사용하기 때문이다. (책에서는 각 enum 요소가 1,2,4,8,16,32 로 설정됨
GPT 답변:
1. Days 열거형의 문제점
enum Days
{
SUNDAY, // 0
MONDAY, // 1
TUESDAY, // 2
WEDNESDAY, // 3
THURSDAY, // 4
FRIDAY, // 5
SATURDAY // 6
};
각 열거형 멤버는 기본적으로 int 값(0부터 시작하는 정수)을 갖습니다.
workingDays에 MONDAY | TUESDAY | THURSDAY를 할당하면:
Days workingDays = Days.MONDAY | Days.TUESDAY | Days.THURSDAY;
이 연산은 비트 OR 연산을 기대하지만, Days 열거형 값이 비트 플래그(즉, 1, 2, 4, 8 같은 2의 제곱수)로 정의되지 않았기 때문에 예상과 다르게 동작할 수 있다.
2. HasFlag(Days.SATURDAY) 문제 분석
- Days.MONDAY | Days.TUESDAY | Days.THURSDAY를 수행하면 내부적으로
1 | 2 | 4 = 7 (즉, 0111)
- workingDays의 값은 7이 됩니다.
- Days.SATURDAY의 값은 6입니다.
- HasFlag는 비트 플래그 열거형을 가정하고 동작하는데, workingDays 값 7(0111)이 Days.SATURDAY(6, 즉 0110)을 포함한다고 판단하여 True를 반환합니다.
즉, HasFlag는 내부적으로 (workingDays & Days.SATURDAY) == Days.SATURDAY 같은 방식으로 평가하는데, 7 & 6은 6이므로 참이 됩니다.
3. 해결 방법: [Flags] 적용 및 값 변경
비트 필드 형태로 정의하려면 Days 열거형을 다음과 같이 변경해야 합니다:
[Flags]
enum Days
{
SUNDAY = 1 << 0, // 1
MONDAY = 1 << 1, // 2
TUESDAY = 1 << 2, // 4
WEDNESDAY = 1 << 3,// 8
THURSDAY = 1 << 4, // 16
FRIDAY = 1 << 5, // 32
SATURDAY = 1 << 6 // 64
};
이제 workingDays를 정의하고 HasFlag를 실행하면 올바르게 동작합니다:
Days workingDays = Days.MONDAY | Days.TUESDAY | Days.THURSDAY;
Console.WriteLine(workingDays.HasFlag(Days.SATURDAY)); // False
Console.WriteLine(workingDays.HasFlag(Days.MONDAY)); // True
Console.WriteLine(workingDays); // MONDAY, TUESDAY, THURSDAY
✅ Days.SATURDAY는 workingDays에 포함되지 않으므로 False가 출력됩니다.
✅ 이제 HasFlag 메서드가 제대로 동작합니다.
💡 결론
- [Flags] 속성을 추가하고, 열거형 값을 비트 플래그(2의 제곱수) 로 정의해야 HasFlag가 올바르게 동작합니다.
- 기본적으로 C#의 enum 값은 정수형이므로 HasFlag를 사용할 때 주의해야 합니다.
Days workingDays = Days.MONDAY | Days.TUESDAY | Days.THURSDAY;
Console.WriteLine(workingDays.HasFlag(Days.SATURDAY)); //SUNDAY를 포함하고 있나?
//출력이 False가 아닌 True로 나온다.
Days today = Days.MONDAY;
Console.WriteLine(workingDays.HasFlag(today)); // 출력 : True
Console.WriteLine(workingDays); // 출력: MONDAY TUESDAY THURSDAY
[Flags]
enum Days
{
SUNDAY = 1 << 0, MONDAY = 1 << 1, TUESDAY = 1 << 2, WEDNESDAY = 1 << 3, THURSDAY = 1 << 4, FRIDAY = 1 << 5, SATURDAY = 1 << 6
};
[Flags] 라는 attribute와 이넘 요소의 값들을 비트식으로 만들었더니 출력이 제대로 나온다. 또한 workigngDays가 비트 합이 아닌 문자열로 출력됨을 확인 할 수 있다.
Calc('+', 5, 6); // 더하기 연산을 수행하도록 '+' 전달
int Calc(char opType, int operand1, int operand2)
{
switch (opType)
{
case '+': return operand1 + operand2;
case '-': return operand1 - operand2;
case '*': return operand1 * operand2;
case '/': return operand1 / operand2;
}
return 0;
}
상기 코드는 향후 유지보수하는데 번거로움이 있다. Calc메서드를 사용하는 측에서 도대체 Calc 메서드에서 어떤 연산을 제공하는지 알길이 없으므로 Calc 내부의 코드를 살펴보거나 제공되는 도움말을 참조해야만 한다. 개발자가 실수로 오타를 내면 정상적인 연산이 수행되지 않을 수 있으며, 나머지 연산을 추가 할 경우 Calc 개발자는 이러한 사실을 반드시 타 개발자에게 알려야만한다.
이러한 문제를 enum으로 해결한 코드이다.
Calc(CalcType.Add, 5, 6); // enum 인스턴스로 전달
int Calc(CalcType opType, int operand1, int operand2)
{
switch (opType)
{
case CalcType.Add: return operand1 + operand2;
case CalcType.Minus: return operand1 - operand2;
case CalcType.Multiply: return operand1 * operand2;
case CalcType.Divide: return operand1 / operand2;
}
return 0;
}
enum CalcType { Add, Minus, Multiply, Divide };
CalcType enum정의만 봐도 지원되는 연산을 짐작 가능하고 오타 발생 시 컴파일러가 오류를 발생시킬 것으로 유지보수용으로도 좋다.
4.5.2 멤버 유형 확장
프로퍼티는 메서드의 변형이며 델리게이트는 중첩 클래스의 변형이라 할 수 있다.
클래스에서 기본적인 필드, 메서드 외 다양한 구성요소를 알아보자.
4.5.2.1 읽기 전용 필드
readonly는 일반 메서드에서 값 대입 시 컴파일에러가 발생되며, 선언 시 초기화 또는 생성자에서 값을 초기화 할 수 있다.
읽기 전용 필드는 변수 정의 할 떄 또는 생성자 내부에서 그 값을 한번만 초기화할 수 있으며 그 외에서는 초기화가 안된다.
가변객체(mutable object) : 객체의 상태가 변할 수 있는 객체.
불변객체(immutable object): 객체의 상태가 불변인 객체
메서드안에서 값을 변경하거나 setter와 같은 프로퍼티가 없는 즉 객체의 내부값을 변경할 수 없는 클래스의 경우 불변객체에 해당된다. 불변 타입을 만들때 readonly 예약어가 유용할 수 있다.
4.5.2.2 상수
상수는 리터럴에 식별자를 붙인 것이라 할 수 있다.
변하는 값을 고정된 식별자로 가리키는 것이 변수라하면 상수는 변치 않는 값인 리터럴을 식별자로 재사용할 수 있게 만들어준다.
readonly 변수와 다른 상수 특징
1. 상수는 static 예약어가 허용되지 않는다.(의미상 이미 static에 해당)
2. 3.1절 '기본 자료형'에서 다룬 형식에 대해서만 상수 정의가 허용.
3. 반드시 상수 정의와 함께 값 대입해야한다. 즉 생성자에서 접근 불가.
4. 상수는 컴파일할 때 해당 소스코드에 값이 직접 치환되는 방식으로 구현된다.
기본타입에선 MaxValue, MinValue라는 공통된 상수를 제공한다.
- sbyte.MinValue, sbyte.MaxValue, byte.Minvalue, byte.MaxValue, .... , decimal.MinValue, decimal.MaxValue
숫자형 상수는 서로 연관된 것들끼리 모아서 enum 타입으로 정리할 수 있다.
const int Sunday = 0;
const int Monday = 1;
const int Tuesday = 2;
...
const int Saturday = 6;
//상수를 enum 타입으로 묶어서 표현
enum Days
{
Sunday, Monday, Thuesday ... , Saturday
}
4.5.2.3 이벤트
이벤트는 델리게이트처럼 타입의 간편 표기법이다. 다음 조건을 만족하는 정형화된 콜백 패턴을 구현 할 때 event 예약어를 사용하면 코드를 줄 일 수 있다.
1. 클래스에서 이벤트(콜백)를 제공한다.
2. 외부에서 자유롭게 해당 이벤트(콜백)를 구독하거나 해지하는 것이 가능하다.
3. 외부에서 구독/해지는 가능하지만, 이벤트 발생은 오직 내부에서만 가능하다.
4. 이벤트(콜백)의 첫 번째 인자로는 이벤트를 발생시킨 타입의 인스턴스다.
5. 이벤트(콜백)의 두 번째 인자로는 해당 이벤트에 속한 의미 있는 값이 제공된다.
클래스에서 이벤트 성격의 콜백 수단을 제공하는 것이 목적이므로 기존의 델리게이트를 사용해서도 동일하게 구현 가능하다.
다만 위 조건들에 부합 시 event 예약어로 코드를 적게 사용하는 것이 개발자(클래스를 만든는 개발자와 그 클래스를 사용하는 개발자)에겐 편리한 방법이 된다.
암기할정도로 익숙해지자 !
소수 생성기
class CallbackArg { } // 콜백의 값을 담는 클래스의 최상위 부모 클래스 역할
class PrimeCallbackArg : CallbackArg
{
public int Prime;
public PrimeCallbackArg(int prime)
{
this.Prime = prime;
}
}
//소수 생성기: 소수가 발생할 때마다 등록된 콜백 메서드 호출
class PrimeGenerator
{
//콜백을 위한 델리게이트 타입 정의
public delegate void PrimeDelegate(object sender, CallbackArg arg);
//콜백 메서드를 보관하는 델리게이트 인스턴스 필드
PrimeDelegate callbacks;
//콜백 메서드 추가
public void AddDelegate(PrimeDelegate callback)
{
callbacks = Delegate.Combine(callbacks, callback) as PrimeDelegate;
}
//콜백 메서드 삭제
public void RemoveDelegate(PrimeDelegate callback)
{
callbacks = Delegate.Remove(callbacks, callback) as PrimeDelegate;
}
//주어진 수까지 루프를 돌면서 소수가 발견되면 콜백 메서드 호출
public void Run(int limit)
{
for(int idx = 2; idx <= limit; ++idx)
{
if(IsPrime(idx) == true && callbacks != null)
{
callbacks(this, new PrimeCallbackArg(idx));
}
}
return;
}
//소수 판정 메서드
private bool IsPrime(int candidate)
{
if ((candidate & 1) == 0)
{
return candidate == 2;
}
for(int idx = 3; (idx * idx) <= candidate; idx += 2)
{
if ((candidate % idx) == 0) return false;
}
return candidate != 1;
}
}
class Program
{
//콜백으로 등록될 메서드1
static void PrintPrime(object sender, CallbackArg arg)
{
Console.Write((arg as PrimeCallbackArg).Prime +", ");
}
static int Sum;
//콜백으로 등록될 메서드2
static void SumPrime(object sender, CallbackArg arg)
{
Sum += (arg as PrimeCallbackArg).Prime;
}
static void Main(string[] args)
{
PrimeGenerator gen = new();
//PrintPrime 콜백 메서드 추가
PrimeGenerator.PrimeDelegate callprint = PrintPrime;
gen.AddDelegate(callprint);
//SumPrime 콜백 메서드 추가
PrimeGenerator.PrimeDelegate callsum = SumPrime;
gen.AddDelegate(callsum);
//1~10까지 소수를 구하고
gen.Run(10);
Console.WriteLine();
Console.WriteLine(Sum);
//SumPrime 콜백 메서드를 제거한 후 다시 1~15까지 소수를 구하는 메서드 호출
gen.RemoveDelegate(callsum);
gen.Run(15);
}
}
#출력 값
2, 3, 5, 7
17
2, 3, 5, 7, 11, 13
PrimeGenerator 타입은 소수가 발견될 때마다 콜백을 발생시키며, 외부에서 이 콜백에 관심이 있다면 구독하고 필용벗어지면 다시 해지할 수 있는 수단을 제공한다.
event 를 사용한 콜백 예제.
PrimeCallbackArg 타입이 상속받는 CallbackArg 타입이 필요없다. 여기에 대응되는 System.EventArgs 라는 타입이 닷넷에서 제공되기에 곧바로 EventArgs에서 상속받는 것으로 처리할 수 있다.
class PrimeCallbackArg : EventArgs // 콜백 값을 담는 클래스 정의
{
public int Prime;
public PrimeCallbackArg(int prime)
{
this.Prime = prime;
}
}
따라서 콜백 메서드에 전달되는 인자를 기존의 CallbackArg에서 EventArgs로 변경한다.
static void PrintPrime(object sender, EventArgs arg)
{
Console.WriteLine((arg as PrimeCallbackArg).Prime +", ");
}
static void SumPrime(object sender, EventArgs arg)
{
Sum += (arg as PrimeCallbackArg).Prime;
}
PrimeGenerator 타입에 구현되어 있는 PrimeDelegate, AddDelegate, RemoveDelegate 멤버를 제거하고 다음 한줄로 정의하면 된다.
public event EventHandler PrimeGenerated;
callbacks 인자의 이름이 이벤트의 PrimeGenerated로 바뀜에 따라 Run 메서드의 코드도 변경하자.
public void Run(int limit)
{
for(int idx = 2; idx < limit; ++idx)
{
if(IsPrime(idx) == true && PrimeGenerated != null)
{
PrimeGenerated(this, new PrimeCallbackArg(idx));
}
}
}
이벤트를 사용하는 코드를 보자. 이전보다 더욱 간결히 이벤트를 구독/해지가 가능하다.
PrimeGenerator gen = new();
gen.PrimeGenerated += PrintPrime; // PrintPrime 메서드로 이벤트 구독
gen.PrimeGenerated += SumPrime; // SumPrime 메서드로 이벤트 구독
gen.Run(10);
Console.WriteLine();
Console.WriteLine(Sum);
gen.PrimeGenerated -= SumPrime; // SumPrime 메서드의 이벤트 해지
gen.Run(15);
최종 소스코드
예제 4.28 delegate 대신 event 예약어를 사용한 예제
class PrimeCallbackArg : EventArgs
{
public int Prime;
public PrimeCallbackArg(int prime)
{
this.Prime = prime;
}
}
//소수 생성기: 소수가 발생할 때 마다 등록된 콜백 메서드를 호출
class PrimeGenerator
{
public event EventHandler PrimeGenerated;
public void Run(int limit)
{
for(int idx = 2; idx <= limit; idx++)
{
if(IsPrime(idx) == true && PrimeGenerated != null)
{
//콜백을 발생시킨 측의 인스턴스와 발견된 소수를 콜백 메서드에 전달
PrimeGenerated(this, new PrimeCallbackArg(idx));
}
}
}
private bool IsPrime(int value)
{
if((value &1) ==0) return value == 2;
for(int idx = 3; idx*idx <= value; idx += 2)
{
if (value % idx == 0) return false;
}
return value != 1;
}
}
class Program
{
// 콜백으로 등록될 메서드 1
static void PrintPrime(object sender, EventArgs arg)
{
Console.Write((arg as PrimeCallbackArg).Prime + ", ");
}
static int Sum;
//콜백으로 등록될 메서드 2
static void SumPrime(object sender, EventArgs arg)
{
Sum += (arg as PrimeCallbackArg).Prime;
}
static void Main(string[] args)
{
PrimeGenerator gen = new();
//콜백 메서드 추가
gen.PrimeGenerated += PrintPrime;
gen.PrimeGenerated += SumPrime;
gen.Run(10);
Console.WriteLine();
Console.WriteLine(Sum);
gen.PrimeGenerated -= SumPrime;
gen.Run(15);
}
}
이벤트는 델리게이트의 패턴을 일반화해서 제공하는 것으로 다음과 같은 구문이다.
class 클래스_명
{
접근_제한자 event EventHandler 식별자;
}
# 설명: 클래스의 멤버로 이벤트를 정의한다. 이벤트는 외부에서 구독/해지가 가능하고 내부에서 이벤트를 발생시키면
외부에서 다중으로 이벤트에 대한 콜백이 발생할 수 있다.
4.5.2.4 인덱서
배열의 요소에 접근시 다음과 같은 구문을 사용할 수 있다.
int[] intArray = new int[5];
intArray[0] = 6; //0번째 요소 접근
클래스에서 배열 intArray[0] 처럼 요소 접근 하는 방법은 this 예약어를 이용한 인덱서를 사용한다.
class 클래스_명
{
접근_제한자 반환타입 this[인덱스타입 인덱스식별자]
{
접근_제한자 get
{
//..[코드]..
return 반환타입과 일치하는 유형의 표현식
}
접근_제한자 set
{
//인덱스 식별자로 구분되는 값에 value를 대입
}
}
}
#설명: 인덱스를 이용하면 클래스의 인스턴스 변수에 배열처럼 접근하는 방식의 대괄호 연산자를 사용 할 수 있다.
프로퍼티를 정의하는 구문과 유사하며, 단지 프로퍼티명이 this예약어로 대체된다는 점과 인덱스로 별도의 타입을 지정
할 수 있다는 점이 다르다.
클래스 내부에 인덱서를 제공하면 배열을 접근할 때의 대괄호 연산자 사용을 클래스의 인스턴스에 대해서도 동일하게 사용 할 수 있다.
class IntegerText
{
char[] txtNumber;
public IntegerText(int number)
{
//Int32 타입을 System.String으로 변환, 다시 String에서 char 배열로 변환
this.txtNumber = number.ToString().ToCharArray();
}
//인덱서를 사용해 숫자의 자릿수에 따른 문자를 반환하거나 치환
public char this[int index]
{
get
{
//1의 자릿수는 숫자에서 가장 마지막 단어를 뜻하므로 역으로 인덱스를 다시 계산
return txtNumber[txtNumber.Length - index - 1];
}
set
{
//특정 자릿수를 숫자에 해당하는 문자로 치환 가능
txtNumber[txtNumber.Length - index - 1] = value;
}
}
//char 배열 -> String으로 전환 (용도: 문자열 길이 파악 목적)
public override string ToString() => new string(txtNumber);
public int ToInt32() => Int32.Parse(ToString());
}
class Program
{
static void Main(string[] args)
{
IntegerText aInt = new IntegerText(123456);
int step = 1;
for(int idx =0; idx < aInt.ToString().Length; ++idx)
{
Console.WriteLine(step + "의 자릿수: " + aInt[idx]);
step *= 10;
}
aInt[0] = '9';
Console.WriteLine(aInt.ToInt32());
}
}
#출력
1의 자릿수: 6
10의 자릿수: 5
100의 자릿수: 4
1000의 자릿수: 3
10000의 자릿수: 2
100000의 자릿수: 1
125459
위 코드에서 사용된 인덱서 구문에는 index 변수 타입이 int로 돼있는데, 필요 시 다른 타입으로 지정 가능하다.
또한 property처럼 set구문을 제거하여 읽기 전용으로도 만들 수 있다.
Notebook normal = new(13, 4);
Console.WriteLine($"모니터 인치: {normal["인치"]} \"");
Console.WriteLine($"메모리 크기: {normal["메모리크기"]} GB");
class Notebook
{
int inch;
int memoryGB;
public Notebook(int inch, int memoryGB)
{
this.inch = inch;
this.memoryGB = memoryGB;
}
public int this[string propertyName]
{
get
{
switch (propertyName)
{
case "인치":
return inch;
case "메모리크기":
return memoryGB;
}
return -1;
}
}
}
클래스에 배열과 같은 식으로 접근 할 필요가 있을 때 인덱서 사용이 바람직하다.
클래스를 사용하는 입장에서 직관적으로 인덱스를 사용하지 않는 것이 좋다고 생각되면 구현 노력에 비해 사용 빈도가 다소 떨어질 수 있다.
식별자 VS 예약어
✅ 식별자 (Identifier)
- 프로그래머가 직접 이름을 지어주는 것이에요.
- 변수, 함수, 클래스, 인터페이스 등 무언가를 정의할 때 사용하는 이름이에요.
- 예: myVariable, GetUserInfo(), User, studentScore 등
- 규칙:
- 숫자로 시작할 수 없어요.
- 공백을 포함할 수 없어요.
- 특수 문자는 _와 @만 사용 가능해요.
- 예약어는 식별자로 쓸 수 없어요, 다만 @를 붙이면 가능하긴 해요. 예: @class, @int 등
🔒 예약어 (Reserved Word)
- C# 언어 자체에서 미리 정해놓은 특별한 의미를 가진 단어예요.
- 예: int, if, class, return, public, for, void 등
- 예약어는 특정한 문법을 표현하기 위해 사용되며, 식별자로 사용할 수 없어요.
- 다만, 정말 써야 한다면 @를 앞에 붙여서 사용할 수 있어요. (예: @class)
👀 차이 요약
누가 정하나 | 프로그래머가 정함 | C# 언어에서 미리 정해놓음 |
용도 | 변수, 함수, 클래스 등의 이름 | 문법 구조를 구성하는 단어 |
예시 | userName, GetData() | if, else, while, int |
사용 가능성 | 자유롭게 지을 수 있음 | 식별자로 사용 불가 (@ 예외) |
객체 지향 파트에서 배운 예약어
예약어 | - using, namespace - class, interface, struct, enum - private, protected, public, internal - return - this, base - typeof - delegate, event - virtual, override - as, is - sealed, abstract - operator, implicit, explicit - static, const, readonly - ref, out |
문맥 예약어(contextual keywords) | get, set, value |
일반예약어는 C# 으로 작성한 모든 영역의 코드에서 식별자로 사용할 수 없지만, 문맥 예약어는 특정한 상황을 제외하고는 식별자로 쓰는 것이 가능하다.
int get = 0;
Console.WriteLine(get);
int set = 5;
Console.WriteLine(set);
int value = 12;
Console.WriteLine(value);
#다른 예약어는 식별자로 사용 시 런타임에러 발생하나 위의 문맥 예약어는 식별자로 사용가능하다,
get/set/value 예약어는 오직 프로퍼티 구문에서만 예약어로 처리된다. 그 밖의 코드에선 식별자로 사용 가능하다.
문맥 예약어 탄생 계기
c# 5.0에 추가 된 await 문맥 예약어를 c# 4.0으로 이미 만들어 놓은 기존 프로그램에서 변수로 사용한다고 하면, 문맥 예약어의 개념이 없었다면 c# 5.0에서 기존의 4.0 소스를 컴파일 할 경우 컴파일 에러가 날 것이다. 이유는 4.0 소스코드에서 await 변수가 c# 5.0에서 예약어로 바뀌었기 때문이다. 이 문제를 방지하고자 문맥 예약어 키워드를 추가했고 c# 2.0 부터 이런 식으로 확장해서 하위 버전의 소스코드를 문제없이 컴파일 가능하게 했다.
'C#(.Net)' 카테고리의 다른 글
[시작하세요 C# 12 프로그래밍 ] #5 C# 1.0 완성하기 #2 (0) | 2025.03.22 |
---|---|
[시작하세요 C# 12 프로그래밍 ] #5 C# 1.0 완성하기 #1 (0) | 2025.03.21 |
Field(클래스 내부 멤버 변수) vs Property(속성) vs Attribute(특성) (0) | 2025.03.18 |
[시작하세요 C# 12 프로그래밍 ] #4 CSharp 객체 지향 문법 2 (0) | 2025.03.16 |
[시작하세요 C# 12 프로그래밍 ] #4 CSharp 객체 지향 문법 (0) | 2025.03.10 |