구조체와 클래스가 아닌 기본 자료형에도 참조에 의한 호출을 사용할 수 있다.
다음 예제는 ref예약어를 사용해 메서드 호출한 측의 두 변수의 값을 바꾸는 동작을 보여준다.
using System;
namespace Week2_2
{
class Program
{
static void Main(string[] args)
{
int value1 = 5;
int value2 = 10;
SwapValue(ref value1, ref value2);
Console.WriteLine("value1 == " + value1 + ", value2 == " + value2);
}
private static void SwapValue(ref int value1, ref int value2)
{
int temp = value1;
value1 = value2;
value2 = temp;
}
}
}
-----------출력 결과-----------------
value1 == 10, value2 == 5
ref 예약어를 빼면 메서드 내부에서만 바뀔 뿐 외부의 변수에 대해서는 값이 바뀌지 않는다.
메서드에 ref인자로 전달되는 변수는 호출하는 측에서 반드시 값을 할당해야 한다.
할당될 값으로는 null이든 new든 상관없이 어떤 값이든 지정하기만 하면 된다.
int value1; // 값이 없으므로 ref 인자로 전달할 수 없음
string text = null; // null 값을 가지므로 ref 인자로 전달 가능
int value2;
value2 = 5; // 메서드 호출 전, 값을 가진다면 ref 인자로 전달 가능
Vector vt;
vt.X = 5; // Y값이 초기화되지 않았으므로 ref 인자로 부적절
Vector vt2 = new Vector(); // X,Y 필드가 0으로 초기화 되었으므로 ref 인자로 전달 가능
out 예약어
ref와 차이점
- out으로 지정된 인자에 넘길 변수는 초기화되지 않아도 된다. 초기화돼 있더라도 out 인자를 받는 메서드에서는 그 값을 사용할 수 없다.
- out으로 지정된 인자를 받는 메서드는 반드시 변수에 값을 넣어서 반환해야 한다.
out 예약어를 사용하는 곳에 ref 예약어를 사용해 구현하는 것도 가능하다. 즉, out 예약어는 ref 예약어의 기능
가운데 몇 가지를 강제로 제한함으로써 개발자가 좀 더 특별한 용도로 사용하게 끔 일부러 제공된 것이다.
어떤 용도가 out 예약어를 쓰기에 적합할까?
예를 들어, 메서드는 단 1개의 반환값만 가질 수 있지만 out으로 지정된 매개변수를 사용함으로써 여러 개의 값을 반환할 수 있다. 나눗셈을 수행하는 메서드를 구현할 때 한 가지 제약이 있는데 분모를 0으로 둘 수 없다는 것이다.
따라서 분모가 0인 경우와 아닌 경우를 따로 반환값을 설정해야 한다고 가정하자.
int Divide(int n1, int n2)
{
if (n2 == 0) // 분모가 0이면 나눗셈 결과로 0을 반환
{
return 0;
}
return n1 / n2;
}
얼핏 보면 타당해 보이지만 분모가 0이어서 0을 반환하는 것과 분자가 0이어서 0을 반환하는 경우가 겹친다.
이를 더 나은 코드로 개선하려면 나누기를 할 수 있는지 여부를 함께 불린형으로 반환해야 한다.
이때 구조체를 통해 이를 구현할 수 있다.
struct DivideResult
{
public bool Success;
public int Result;
}
DivideResult Divide(int n1, int n2)
{
DivideResult ret = new DivideResult();
if (n2 == 0)
{
ret.Success = false;
return ret;
}
ret.Success = true;
ret.Result = n1 / n2;
return ret;
}
out 예약어로 개선할 수 있다.
static bool Divide(int n1, int n2, out int result)
{
if (n2 == 0)
{
result = 0;
return false;
}
result = n1 / n2;
return true;
}
static void Main(string[] args)
{
int quotient;
if(Divide(15,3,out quotient)==true)
{
Console.WriteLine(quotient); // 출력 5
}
}
out 예약어가 참조에 의한 호출로 값을 넘기지 않는다면 위와 같은 구현은 가능하지 않다는 점을 상기하자.
out으로 지정된 result 변수는 메서드가 return 하기 전에 반드시 초기화돼 있어야 한다.
만약 5번째 줄의 result = 0;을 제거하면 6번째 줄의 return 시점에 초기화되지 않았다는 이유로 빌드할 때 오류가 난다.
이와 유사한 용도로 닷넷 프레임워크에서는 각 기본 타입에 TryParse라는 메서드를 제공한다.
// System.Int32 타입에 정의된 TryParse 정적 메서드
public static bool TryParse(string s, out int result);
이 메서드는 변환이 성공했는지 여부를 true/false로 반환하고, 변환이 성공했다면 out으로 지정된 result 변수에 값을 반환한다.
int n;
if ( int.TryParse("123456", out n) == true) // System.Int32의 TryParse 호출
{
Console.WriteLine(n); // 출력 결과 : 123456
}
double d;
if(double.TryParse("12E3",out d) == true) // double은 지수 표기법의 문자열도 지원
{
Console.WriteLine(d); // 출력 결과 : 12000
}
ref는 메서드를 호출하는 측에서 변수의 값을 초기화함으로써 메서드 측에 의미 있는 값을 전달한다.
반면 out은 메서드 측에서 반드시 값을 할당해서 반환함으로써 메서드를 호출한 측에 의미 있는 값을 반환한다.
ref도 out처럼 참조에 의한 전달이기 때문에 메서드 측에서 의미 있는 값을 호출하는 측에 전달할 수 있다.
열거형
열거형도 값 형식의 하나로 byte, sbyte, short, ushort, int, uint, long, ulong만을 상속받아 정의할 수 있는
제한된 사용자 정의 타입이다.
enum Days
{
Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday
}
class Program
{
static void Main(string[] args)
{
Days today = Days.Sunday;
Console.WriteLine(today);
}
}
enum은 내부에 정의된 식별자 순서에 따라 각각 0부터 1씩 값을 증가시킨다. Sunday는 0이고 Saturday는 6이다.
결국 상속받은 System.Int32 타입에 해당하는 값이 된다. 그런데 enum 변수를 출력했을 때 숫자 0이 아닌
Sunday가 출력되는 이유는 뭘까? enum의 조상이 System.Object임을 감안하면 당연히 enum은 ToString을 재정의 하였고,
숫자값이 아닌 문자열로 반환해 준다.
System.Int32를 부모로 두기 때문에 Days 타입은 int를 비롯해 각종 숫자형 타입과 형변환하는 것이 가능하고 그 반대도
마찬가지다. 단지 제약이라면 암시적 형변환이 아닌 명시적 형변환을 해야 한다는 것뿐이다.
Days today = Days.Sunday;
int n = (int)today;
short s = (short)today;
today = (Days)5;
Console.WriteLine(today); // 출력 : Friday
enum의 시작 요소 값에 0이 아닌 다른 정수를 지정할 수도 있고 그 이후의 요소에 대해서도 1씩 증가하는 것이 아닌
임의로 값을 정할 수 있다.
Days 인스턴스가 작업일 이라는 것을 나타내기 위해 Monday부터 Friday까지의 값을 담을 수 있다.
이때 각 요소에 대한 값을 2의 배수로 지정한다.
enum Days
{
Sunday = 1, Monday = 2, Tuesday = 4, Wednesday = 8,
Thursday = 16, Friday = 32, Saturday = 64
}
이렇게 정의하면 다음과 같이 |(OR) 연산자를 사용해 정수형 값을 겹칠 수 있고, HasFlag 메서드를 사용해
특정 요소 값을 포함하고 있는지도 판단할 수 있다.
Days workingDays = Days.Monday | Days.Tuesday | Days.Wednesday | Days.Thursday | Days.Friday;
Console.WriteLine(workingDays.HasFlag(Days.Sunday)); // Sunday를 포함하고 있는가?
Days today = Days.Friday;
Console.WriteLine(workingDays.HasFlag(today)); // today를 포함하고 있는가?
Console.WriteLine(workingDays);
---------------출력 결과----------------
False
True
62
마지막 62 값은 enum 타입이라는 점을 감안할 때 Monday, Tuesday, Wednesday, Thursday, Friday가 적절한데,
이런 식으로 enumm 타입의 인스턴스가 여러 개의 값을 포함하는 용도로 사용된다는 것을 알리기 위해
[Flags] 특성을 지정할 수 있다.
[Flags] 특성은 enum 타입에만 사용될 수 있고 다음과 같이 타입 정의를 할 때 함께 지정한다.
[Flags]
enum Days
{
Sunday = 1, Monday = 2, Tuesday = 4, Wednesday = 8,
Thursday = 16, Friday = 32, Saturday = 64
}
이렇게 바꾸고 다시 실행하면 62 대신 Monday, Tuesday, Wednesday, Thursday, Friday 문자열이 출력된다.
멤버 유형 확장
클래스 안에는 필드 메서드 외에 다양한 구성요소가 있다
읽기 전용 필드
프로퍼티를 이용하면 필드의 값을 읽기만 가능하도록 외부에 노출할 수 있다. 하지만 클래스 내부에서도 읽기만
가능하도록 만들고 싶다면 어떻게 해야 할까? 또는 한 번 값을 쓴 후 다시 값을 설정하지 못하게 만들고 싶을 수도 있다.
이런 경우에 readonly 예약어를 사용해 읽기 전용 필드를 정의하면 된다.
public class Scheduler
{
readonly int second = 1; // 읽기 전용 필드 정의 및 값을 대입
readonly string name; // 읽기 전용 필드 정의
public Scheduler()
{
this.name = "일정관리"; // 읽기 전용 필드는 생성자에서도 대입 가능
}
public void Run()
{
this.second = 5; // 컴파일 오류 발생! 일반 메서드에서 값을 대입할 수 없다.
}
}
읽기 전용 필드는 변수를 정의할 때와 생성자 내부를 제외하고는 그 값을 바꿀 수 없다.
기본적으로 모든 필드는 값이 변할 수 있다. 대란 말로 하면 객체의 상태가 변할 수 있는 것인데 이를 가변 객체라고 한다.
반면 한번 지정되면 다시 바뀔 수 없는 경우 이를 구분하여 불변 객체라 한다.
불변 타입을 만들 때 readonly 예약어가 도움이 된다. 클래스 내부적으로 불변 상태를 보장하여 유지보수 시 실수를 줄일 수 있기 때문.
상수
중복되는 리터럴을 쓰는 경우가 빈번하게 발생한다. 나중에 요구사항이 변경되어 수정이 필요할 때 해당 문자열을 하나씩 고쳐야 하는 불편함이 있다. 이런 경우 상수를 사용해 표현하면 변경해야 할 문자열이 한 군데에 있으므로 소스코드를
유지보수하기 쉬워진다.
class Program
{
const string TEXT = " 변수의 값: ";
static viud Main(string[] args)
{
int x = 5;
int y = 10;
Console.WriteLine("x" + TEXT + x);
Console.WriteLine("y" + TEXT + y);
}
}
readonly와 비슷하지만 몇 가지 차이점이 있다.
- 상수는 static 예약어가 허용되지 않는다 (이미 static이다)
- 기본 자료형에서 다른 형식에 대해서만 상수 정의가 허용된다.
- 반드시 상수 정의와 함께 값을 대입해야 한다. 즉, 생성자에 접근할 수 없다.
- 상수는 컴파일할 때 해당 소스코드에 값이 직접 치환되는 방식으로 구현된다.
이벤트
간편 표기법 중 하나로 다음 조건을 만족하는 정형화된 콜백 패턴을 구현하려고 할 때
event 예약어를 사용하면 코드를 줄일 수 있다.
- 클래스에서 이벤트 (콜백)을 제공한다.
- 외부에서 자유롭게 해당 이벤트(콜백)를 구독하거나 해지하는 것이 가능하다.
- 외부에서 구독/해지는 가능하지만 이벤트 발생은 오직 내부에서만 가능하다.
- 이벤트(콜백)의 첫 번째 인자는 이벤트를 발생시킨 타입의 인스턴스다.
- 이벤트(콜백)의 두 번째 인자로는 해당 이벤트에 속한 의미 있는 값이 제공된다.
물론 클래스에서 이벤트 성격의 콜백 수단을 제공하는 것이 목적이므로 기존의 델리게이트를 사용해서도
동일하게 구현할 수 있다. 단지 그것이 위의 패턴에 부합한다면 event 예약어로 코드를 적게 사용하는 것이
해당 클래스를 만드는 개발자뿐 아니라 사용하는 개발자에게도 편리한 방법이다.
소수 생성기를 구현해 보자. 1부터 n까지 값을 확인하면서 소수라고 판정될 때마다 콜백을 발생시키는 클래스를
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 i = 2; i <= limit; i++)
{
if(IsPrime(i) == true && callbacks != null)
{
// 콜백을 발생시킨 측의 인스턴스와 발견된 소수를 콜백 메서드에 전달
callbacks(this, new PrimeCallbackArg(i));
}
}
}
// 소수판정 메서드
private bool IsPrime(int candidate)
{
if((candidate & 1) == 0)
{
return candidate == 2;
}
for (int i = 3; (i * i) <= candidate; i += 2){
if ((candidate % i) == 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 PrimeGenerator();
// 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~1 까지 소수를 구하는 메서드 호출
gen.RemoveDelegate(callsum);
gen.Run(15);
}
}
PrimeGenerator 타입은 소수가 발견될 때마다 콜백을 발생시키고 있으며, 외부에서 이 콜백에 관심이 있다면 구독하고, 필요 없어지면 다시 해지할 수 있는 수단을 제공한다.
이제 event를 사용해 예제를 간결하게 만들어보자. 우선 PrimeCallbackArg 타입이 상속받는 CallbackArg 타입이 필요 없다.
여기에 대응되는 System.EventArgs라는 타입이 이미 닷넷 프레임워크에서 제공되고 있으므로 곧바로 EventArgs에서
상속받는 것으로 처리할 수 있다.
using System;
namespace Week2_2
{
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 PrimeGenerator();
gen.PrimeGenerated += PrintPrime; // PrintPrime 메서드로 이벤트 구독
gen.PrimeGenerated += SumPrime; // SumPrime 메서드로 이벤트 구독
// 1~10까지 소수를 구하고,
gen.Run(10);
Console.WriteLine();
Console.WriteLine(Sum);
gen.PrimeGenerated -= SumPrime; // SumPrime 메서드의 이벤트 해지
gen.Run(15);
}
}
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 i = 2; i <= limit; i++)
{
if(IsPrime(i) == true && PrimeGenerated != null)
{
// 콜백을 발생시킨 측의 인스턴스와 발견된 소수를 콜백 메서드에 전달
PrimeGenerated(this, new PrimeCallbackArg(i));
}
}
}
// 소수판정 메서드
private bool IsPrime(int candidate)
{
if((candidate & 1) == 0)
{
return candidate == 2;
}
for (int i = 3; (i * i) <= candidate; i += 2){
if ((candidate % i) == 0) return false;
}
return candidate != 1;
}
}
}
결국 이벤트는 델리게이트의 사용 패턴을 좀 더 일반화해서 제공하는 것으로 다음과 같이 간단하게 구문이 요약된다.
class 클래스_명
{
접근_제한자 event EventHandler 식별자;
}
// 클래스의 멤버로 이벤트를 정의한다. 이벤트는 외부에서 구독.해지가 가능하고,
// 내부에서 이벤트를 발생시키면 외부에서 다중으로 이벤트에 대한 콜백이 발생할 수 있다.
이벤트는 GUI를 제공하는 응용 프로그램에서 매우 일반적으로 사용된다. 예를 들어, 윈도우에 포함된 버튼이 있고,
버튼을 눌렀을 때 파일을 생성해야 하는 작업을 한다고 가정해 보자. Button 클래스 제작자는 당연히 Click이라는
이벤트를 구현해 둘 것이고, 버튼을 이용하는 개발자는 Click 이벤트를 구독하는 메서드 내에서 파일 작업을 수행하는
코드를 작성하면 된다.
인덱서
배열이 아닌 일반 클래스에서 n번째 요소에 접근하는 구문을 사용할 수 없을까? 대괄호 연산자를 사용자가
직접 정의할 수는 없다. 이를 보완하기 위해 this예약어를 이용한 인덱서라고 하는 특별한 구문을 제공한다.
인덱서를 이용하면 클래스의 인스턴스 변수에 배열처럼 접근하는 방식의 대괄호 연산자를 사용할 수 있다.
프로퍼티를 정의하는 구문과 유사하며, 단지 프로퍼티명이 this 예약어로 대체된다는 점과 인덱스로 별도의
타입을 지정할 수 있다는 점이 다르다.
클래스 내부에 인덱서를 제공하면 배열을 접근할 때의 대괄호 연산자 사용을 클래스의 인스턴스에 대해서도
동일하게 사용할 수 있다. 다음은 Int32 정수형 데이터의 특정 자릿수를 인덱서를 사용해 문자 (char) 데이터로
다룰 수 있는 예제다.
using System;
namespace Week2_2
{
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;
}
}
public override string ToString()
{
return new string(txtnumber);
}
public int ToInt32()
{
return Int32.Parse(ToString());
}
}
class Program
{
static void Main(string[] args)
{
IntegerText aInt = new IntegerText(123456);
int step = 1;
for (int i = 0; i < aInt.ToString().Length; i++)
{
Console.WriteLine(step + "의 자릿수: " + aInt[i]);
step *= 10;
}
aInt[3] = '5';
Console.WriteLine(aInt.ToInt32());
}
}
}
index 변수 타입이 int로 돼 있는데 프로그램에서 필요하다면 다른 타입으로 지정하는 것이 가능하다.
또한 프로퍼티처럼 set구문을 제거하면 읽기 전용으로 만드는 것도 가능하다.
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.14 BCL - 파일, 스레드, 네트워크 통신 (포트 까지) (0) | 2023.09.14 |
|---|---|
| 2023.09.13 BCL (시간, 문자열, StringBuilder, 정규 표현식, 컬렉션, ArrayList, Hashtable) (0) | 2023.09.13 |
| 2023.09.11 인터페이스를 이용한 콜백 구현 ~ ref...ing (0) | 2023.09.11 |
| 2023.09.08 클래스 간의 형변환~ 인터페이스 자체로 의미부여 (0) | 2023.09.08 |
| 2023.09.07 C# 정리 (0) | 2023.09.07 |