C# Basic 다시 공부
공변성 반공변경 대한 자료를 찾다가 발견한 아래 사이트에서 C#을 복습할 있는 기회가 있어 포스팅
http://www.devbb.net/viewforum.php?f=38&sid=0ed08ea5ae039c4a56164f85650622c1
기본 할당과 변수 유효 범위
C#은 자동적으로 모든 멤버 변수를 안전한 기본 값 '0' 으로 설정한다.
하지만 메소드내 지역변수는 자동으로 기본 값이 할당되지 않기 때문에, 사용전에 초기화가 필요하다.
Code:
using System;
class DefaultValueTester
{
public int a;
public bool b;
public string s;
public object o;
public static int Main()
{
// 생성된 객체 v
의 모든 멤버변수는 자동으로 초기값으로 설정된다.
DefaultValueTester v = new DefaultValueTester();
// 지역변수는 사용전에 초기화가 필요하다.
int localInt;
Console.WriteLine(localInt); // 오류 !!!
return 0;
}
}
값 형식과 참조 형식
모든 숫자 데이타 형식(int, float, 등)과 열거형과 구조체를 포괄하는 값 기반형식은 스택에 할당된다.
따라서 값
형식은 정의된
영역 밖에서는
메모리로부터 곧바로
제거 될
수 있다.
하나의 값 형식을 다른 값 형식에 할당하면, 기본적으로 멤버 대 멤버 복사가 이루어진다.
Code:
using System;
class ValRefClass
{
public static void Main()
{
int i = 99;
int j = i;
j = 88;
}
}
결과
i = 99, j = 88;
구조체의 경우도 마찬가지다.
Code:
using System;
struct FOO
{
public int x, y;
}
class ValRefClass
{
public static void Main()
{
FOO f1 = new FOO();
f1.x = 100;
f1.y = 100;
FOO f2 = f1;
f2.x = 900;
}
}
결과
f1.x = 100, f1.y = 100
f2.y = 900, f2.y = 100
이와는 정반대로, 참조 형식(클래스)은 가비지 컬렉션의 대상인 힙에 할당된다.
힙에 할당된 참조 형식은 가비지 컬렉션에 의해 제거되기 전까지는 계속 메모리에 남아 있게 된다.
참조 형식의 대입은 멤버의 복사가 아닌 할당된 인스턴스가 생성된 메모리를 복사하게 된다.
C++ 의 포인터 끼리의 대입과 같다고 이해하면 된다.
아래의 예제는 위 구조체 예제에서 구조체 대신 클래스를 사용할 때 참조 형식이 되는 예이다.
Code:
using System;
class FOO
{
public int x, y;
}
class ValRefClass
{
public static void Main()
{
FOO f1 = new FOO();
f1.x = 100;
f1.y = 100;
FOO f2 = f1;
f2.x = 900;
}
}
결과
f1.x = 900, f1.y = 100
f2.y = 900, f2.y = 100
시스템 데이터 형식과 C#별칭
모든 내장 C# 데이터 형식은 사실 System 네임스페이스에 정의되어 있는 시스템 형식에 대한 별칭이다.
Attachment:
예를 들면 'int' 는 실제 시스템 형식인 System.Int32
의 약칭 표시가 되는 것이다.
Code:
using System;
class Test
{
public static void Main()
{
System.Int32 a = 1000; // int a = 1000; 와 같음
System.Int32 b = 1000; // int b = 1000; 와 같음
if (a == b)
Console.WriteLine("Same
value!");
}
}
값 형식과 참조 형식간의 변환 : 박싱(boxing), 언박싱(Unboxing)
C# 에는 값 형식과 참조 형식 간의 변환을 간단하게 할 수 있는 메커니즘이 있는데, 이를 박싱(boxing) 이라고 부른다.
값을 박싱한다는
것은 새로운
객체를 힙에
할당하고 새로
할당된 이
인스턴스에 내부
값을 복사하는
것이다. 이때 반환하는
것은 새로
할당된 객체에
대한 참조자이다.
언박싱의 경우에는 반대의 연산이 일어난다. 언박싱은
객체 참조에
들어 있는
값을 이에
상응하는 값
형식으로 변환해
스택에 올려
놓는 과정을
일컫는 용어이다.
여기서 알맞은 데이터 형식으로 언박스하는 것은 상당히 주의를 요하는 작업이며 형식이 틀리다면 InvalidCastException 예외가 발생한다.
Code:
// 값 데이터 형식을 만든다.
short s1 = 25;
// 값을 객체 참조로 박스한다.
object myObj = s1;
// 참조를 이에 상응하는
short 로 언박스(unboxing) 한다.
short s2 = (short)myObj;
try
{
// 박스에 포함된 형식이 string 이 아니라 short 이다.
string str = (string)myObj;
}
catch (InvalidCastException e)
{
Console.WriteLine("OOPS! {0}", e.ToString());
}
C# 의 'out' 키워드
다음은 C# 의 'out' 키워드를 이용해서 두정수의 합을 반환하도록 하는 함수이다.
C# 의 'out' 키워드는 호출자가 하나의 메소드 호출을 통해서 여러 개의 반환 값을 얻을 필요가 있을때 매우 유용하다.
C++ 에서 변수의
포인터를 함수의
매개변수로 넘겨서
결과를 받아
오는 것과
유사하다.
Code:
using System;
class MainClass
{
public static void Add(int x, int y, out int ans)
{
ans = x + y;
}
public static void Main()
{
int x = 9, y = 10, z=0;
Add(x, y, out z);
}
}
결과
x=9, y=10, z=19;
C# 의 'ref' 키워드
참조 매개변수는 호출된 메소드에서, 호출자의
범위 안에 선언된
데이터 포인터에 대한
연산을 하고 싶은
경우에 필요하다.
출력 매개변수(out 키워드)는
인자로 넘기기
전에 초기화될
필요가 없다. 하지만 참조 매개변수는 인자로 넘어가기 전에 반드시 초기화 되어야 한다. 왜냐하면
이미 존재하는
형식에 대한
참조자를 전달해야
하기 때문이다.
Code:
using System;
class MainClass
{
public static void UpperCaseThisString(ref string s)
{
// 문자열을 대문자로 변경해서 반환한다.
s = s.ToUpper();
}
public static void Main()
{
string s = "Can you really have sonic hearing
for $19.00?";
Console.WriteLine("Before: {0} ", s);
UpperCaseThisString(ref s);
Console.WriteLine("After: {0} ", s);
}
}
out : 출력 매개 변수
1. 변수 초기화 필요치 않음 : 출력용 이기 때문에 함수로 전달되기 전 변수값 할당은 무의미 하다
2. 함수내 반드시 변수에 값 할당 : 출력용 이기 때문에 함수내에서 변수 값이 할당 되어야 함.
ref : 참조에 의한 전달
1. 변수 초기화 필요 : 일반 매개변수와 같은 의미
2. 함수내 값 설정하지 않아도 됨
C# 의 'params' 키워드
이 키워드를 이용하면 배열과 같은 다양한 종류의 여러 인수를 하나의 매개변수로서 전달 할 수 있다.
Code:
using System;
class MainClass
{
public static void DisplayArray(string msg, params int[] list)
{
Console.WriteLine(msg);
for (int i = 0; i < list.Length; i++)
Console.WriteLine(list[i]);
[/legend] }
public static void Main()
{
int[] arr = new int[3] { 10, 11, 12 };
DisplayArray("Here is an array of ints",
arr);
}
}
C# 의 구조체
C# 의 구조체도 C++ 형식과 크게 다른것이 없으며, C# 구조체를 별칭으로 하는 동일한 이름의 클래스를 가지지 않는다.
C# 의 클래스와의
차이점은 데이터가
스택에 할당되며
구조체의 대입은
참조가 아닌(주소의
공유가 아닌) 값의
복사가 된다.
클래스와 구조체의 차이점
- 구조체는 값 형식이다.
- 모든 구조체 형식은 암시적으로 System.ValueType 에서 상속된다.
- 구조체 형식의 변수에 대입이 이뤄지면 지정되는 값이 복사된다.
- 구조체의 기본 값은 모든 값 형식 필드를 각 형식의 기본 값으로 설정하고 모든 참조 필드를 null 로 설정한 것이다.
- 구조체 형식과 객체 간의 변환에는 박싱과 언박싱 연산이 필요하다.
- this 의 의미가 다르다.
- 구조체에 대한 인스턴스 필드 선언은 변수 초기화를 포함할 수 없다.
- 구조체는 매개변수가 없는 인스턴스 생성자를 선언할 수 없다.
- 구조체는 소멸자를 선언할 수 없다.
읽기 전용 속성과 쓰기 전용 속성
앞선 예제에서 EmpID 는 읽기/쓰기 속성으로 만들어졌다. 이것을 읽기 전용으로 구성하려면 set 블록을 빼고 만들면 된다.
마찬가지로 쓰기 전용 속성을 만들고 싶으면 get 블록을 빼면 된다.
읽기 전용 필드 만들기
읽기 전용 속성은 읽기 전용 필드와 밀접하게 관련되어 있다. 읽기 전용 필드는 'readonly' 키워드를 이용해 데이터를 보호한다.
읽기 전용 필드는 생성자에서만 할당 가능하며 이 필드에 값을 할당하려고 하면 컴파일 에러가 발생한다.
Code:
public class Employee
{
// 읽기 전용 필드 (생성자에서 할당)
public readonly int empID;
public Employee(int id)
{
// 읽기 전용 필드 할당.
empID = id;
}
}
class MainClass
{
public static void Main()
{
Employee p = new Employee(10);
p.empID = 20; // 읽기 전용 필드이므로 컴파일 에러.
Console.WriteLine(p.empID);
}
};
클래스 멤버 버전 관리
C# 에는 메소드 재정의에 대해 논리적으로 정반대에 해당하는 기능인 메소드 숨기기(hiding)가 있다. .NET *.dll
을 구입해서, 여기에 있는 기본 클래스로부터 새 클래스를 파생시키려 한다고 가정해 보자. 이때 여러분이 만들고자 하는 메소드와 호환되지 않는 같은 이름의 메소드가 기본 클래스에 정의되어 있다면 어떻게 해야 할까?
이런 경우, 바로 객체 사용자가 기본 클래스 구현을 유발하지 않도록 'new' 를 사용하면 된다.
Code:
public class MyBase
{
public virtual void func()
{
Console.WriteLine("Base");
}
}
public class TestClass : MyBase
{
// 이전에 구현된
func() 구현을 모두 감추고 TestClass 고유한 기능을 수행한다.
public new void func()
{
Console.WriteLine("Child");
}
}
class MainClass
{
public static void Main()
{
MyBase c1 = new MyBase();
c1.func();
c1 = new TestClass();
c1.func();
TestClass c2 = new TestClass();
c2.func();
}
};
Output
Attachment:
같은 방법으로 파생된 형식의 필드(멤버변수)에도 new 키워드를 적용해서 상속의 연결 관계에서 동일한 이름으로 정의된 데이터 형식을 숨길 수도 있다.
C# 의 종결 프로세스
모든 형식에 C# 스타일의 소멸자를 만들고 싶을 수도 있다. 그러나 그렇게 하면 안된다.
무엇보다도, 종결자의 역할이 .NET 객체가 비관리 리소스를 제거할 수 있게 하는 것이라는 점을 잊어서는 안된다.
종결 메소드의 사용을 지양해야 하는 좀 더 실질적인 이유는 종결에 시간이 소요된다는 것이다.
new 연산자를 이용해서 관리 힙에 객체를 하나 위치시킬 때, 런타임은 자동적으로 이 객체가 사용자 지정 Finalize() 메소드를 지원하는지 검사하게 된다.
만약 지원한다면 이 객체는 종결 가능한 것으로 표시되고, 이 객체에 대한 포인터가 종료 대기열(finalization queue)이라는 이름의 내부 대기열에 저장된다.
가비지 컬렉터가 객체를 메모리로부터 해제할 때라고 판단할 때, 가비지 컬렉터는 종료 대기열 목록에 기재되어 있는 모든 항목을 검사하고, 이 객체를 힙으로 부터 finalization reachable table 이라는 이름의 또다른 CLR 관리 구조로 복사한다.
이때 별도의 스레드가 실행되어 다음 번 가비지 컬렉션시 테이블에 있는 각 객체의 Finalize() 메소드를 호출한다.
임시 소멸 메소드 만들기
살펴본 것과 같이, 객체 종료 프로세스에는 시간이 소요된다. 이상적으로는, 객체를 만들 때 종료 가능한(finalizable) 것으로 표시되지 않도록 디자인하는 것이 좋다. 그러나 비관리 리소스를 조작하는 형식에서는 이 비관리 리소스들을 필요한 때에 그리고 예측이 가능하게 해제할 수 있어야 한다. 이를 위해서 C# 소멸자를 이용할 수도 있지만, 좀 더 나은 방법도 있다.
사용자 정의 임시 메소드를 정의해서 시스템의 모든 객체가 이 메소드를 구현하도록 하는 방법이 있다.
이 메소드 이름을 Dispose() 라고 했다면고 하자. 이때 주의할 점은, 객체 사용자가 해당 형식의 사용을 끝낼 때 객체 참조가 범위 밖으로 벗어나기 전에 Dispose() 를 직접 호출해야 한다는 점이다.
이 방법을 이용하면, 해당 객체에서 종료 대기열에 접근하거나 가비지 컬렉터가 클래스의 종료 로직을 유발하기를 기다릴 필요 없이 비관리 리소스를 얼마든지 제거할 수 있다.
비관리 리소스가 항상 알맞게 제거될 수 있게 하려면, Dispose() 메소드가 예외를 발생시키지 않고 안전하게 여러 번 호출될 수 있도록 구현해야 한다.
iDisposable 인터페이스
명시적 소멸 루틴을 지원하는 모든 객체들에 일관성을 제공할 수 있도록, .NET 클래스 라이브러리에는 Dispose() 라는 이름의 멤버 하나를 갖는 IDisposable 이라는 이름의 인터페이스가 정의되어 있다. (인터페이스 개념에 대해서는 다른장에서 설명될 것이다.)
Code:
public interface IDisposable
{
public void Dispose();
}
이 방법을 이용하면, 객체 사용자가 얻어왔던 리소스를 가능한 한 빨리 직접 제거하고, 종료 대기열에 들어가서 발생할 오버헤드를 줄일 수 있게 된다. 호출 로직은 간단하다.
Code:
public class Car : IDisposable
{
public void Dispose()
{
// 내부 비관리 리소스 제거
}
}
public class MainClass
{
public static int Main()
{
Car c1 = new Car();
c1.Dispose();
return 0;
}
// c1 은 여전히 힙에 있다가 이 지점에서 컬렉트될 것이다.
}
C# 의 "using" 키워드의 또다른 용도
'using' 키워드는 네임스페이스를 지정하는 용도 이외에 자동으로 Dispose() 메소드를 호출하는 용도로도 사용된다.
IDisposable 인터페이스를 지원하는 객체가 using 블록을 빠져 나갈 때 이 객체의 Dispose() 메소드가 자동적으로 반드시 호출된다.
반면, IDisposable 을 구현하지 않은 형식을 using 블록 안에 지정하면, 컴파일시 에러가 발생한다.
Code:
public void SomeMethod()
{
using (Car c = new Car())
{
// 차를 이용한다.
// using 블록을 나가면 자동적으로 Dispose() 가 호출된다.
}
}
가비지 컬렉션 최적화가비지 컬레션시에 관리 힙에 있는 모든 객체를 그대로 하나씩 다 검사해서 제거할 대상을 찾지 않는다. 이렇게 하면, 특히 큰 응용프로그램의 경우, 시간이 상당히 많이 걸리게 된다.
이러한 컬렉션 과정의 최적화를 위해 힙에 있는 각 객체에 '세대'가 할당된다. 힙에 오랫동안 있는 객체일수록 더 오래 머물러 있을 가능성이 있는반면, 최근에 힙에 배치된 객체일수록 응용 프로그램에서 빨리 해제될 가능성이 높다는 것이다.
각 객체는 다음의 세대 중 하나에 속하게 된다. (.NET 1.1 버전에서)
- 0 세대 : 컬렉션 대상 표시가 된 적이 없는 새로 할당된 객체를 식별한다.
- 1 세대 : 가비지 컬렉션 검색에서 살아남은 객체를 식별한다. (즉, 제거 대상이지만 힙에 충분히 공간이 있어서 아직 제거되지 않은 것을 말한다.)
- 2 세대 : 두 번 이상의 가비지 컬렉션에서 살아남은 객체를 식별한다.
컬렉션이 일어나면, GC 가 우선 모든 0세대 객체를 표시하고 제거한다. 이 결과 필요한 만큼의 메모리가 확보되어 있으면, 남아 있는 객체들이 다음에 사용 가능한 세대로 표시된다.
만약 모든 0세대 객체가 힙으로부터 제거되었는데도 여전히 메모리가 부족하면, 1세대 객체가 표시되고 제거된다. 그 다음에도 메모리가 부족하면 2세대로 넘어간다.
이렇게 해서, 새로운 객체는 금방 제거되는 반면 오래된 객체는 계속 사용되는 것으로 간주된다.
한마디로 말해, GC 는 세대를 토대로 해서 힙영역을 빠르게 해제할 수 있다.
System.GC 형식
System.GC 를 이용하면 가비지 컬렉터와 상호작용할 수 있다.
System.GC 형식 주요 멤버 항목
- Collect() : GC 가 관리 힙에 있는 모든 객체에 대해서 Finalize() 를 호출하게 한다. 원하면 검색할 세대를 지정할 수 있다.
- GetGeneration() : 현재 객체가 속한 세대를 반환한다.
- MaxGeteration : 이 속성은 대상 시스템에서 지원하는 가장 큰 세대를 반환한다.
- ReRegisterForFinalize() : 객체가 다시 finalizable 하게 되도록 한다. 물론 이전에 SuppressFinalize() 에 의해서 종료되지 않도록 표시되었다는 것을 전제로 한다.
- SuppressFinalize() : 해당 객체의 Finalize() 메소드가 호출되지 않게 한다.
- GetTotalMemory() : 현재 힙에 있는 모든 객체들에 의해서 사용되고 있는 메모리와 곧 제거될 객체들의 추산된 합(바이트)을 반환한다. 이 메소드에는 Boolean 매개변수가 있는데, 이 매개변수는 이 메소드 호출시 가비지 컬렉션이 발생할지 아닐지를 지정하는 데 이용한다.
종결될 수 있고(finalizable) 소멸될 수 있는(disposable) 형식 만들기
아래 Car 클래스는 IDisposabe 인터페이스와
C# 스타일의 소멸자를 모두 지원한다.
여기에서는, 엔드 유저가 Dispose() 를 직접 호출하기 때문에, 지정된 객체에 대한 소멸자를 호출할 필요가 없다는 것을 시스템에게 알려주기 위해, Dispose() 메소드가
GC.SuppressFinalize() 를 호출하도록 변경했다.
Code:
public class Car : IDisposable
{
~Car()
{
// 모든 내부 비관리 리소스를 제거한다.
}
public void Dispose()
{
// 모든 내부 비관리 리소스를 제거한다.
...
// 사용자가 Dispose() 를 호출하면 finalize할 필요가 없다.
// 그러므로 finalization 을 억제한다.
GC.SuppressFinalize(this);
}
}
가비지 컬렉션 강제하기
관리 힙이 가득 차면 '언제든지' 자동적으로 가비지 컬렉션이 유발되지만, 원한다면, 정적 메소드인 GC.Collect() 를 이용해서 런타임이 가비지 컬렉션을 수행하도록 강제할 수 있다. 그러나 관리 힙의 본래 의도는 프로그래머의 직접적인 제어를 벗어나서 관리되는 것이므로, 가능하면 가비지 컬렉션을 강제로 수행하지 않는 것이 좋다.
Code:
// 가비지 컬렉션을 강제하고, 각 객체가 finalize 되기를 기다린다.
GC.Collect();
GC.WaitForPendingFinalizers()l
세대와 프로그래밍 방식으로 상호작용하기 예제
Code:
using System;
public class Car : IDisposable
{
private string name;
public Car(string name)
{
this.name = name;
}
~Car()
{
Console.WriteLine("In destruct for {0}!",
name);
}
public void Dispose()
{
Console.WriteLine("In Dispose() for
{0}!", name);
GC.SuppressFinalize(this);
}
}
public class MainClass
{
public static int Main()
{
// 이 차들을 관리 힙에 추가한다.
Car c1, c2, c3, c4;
c1 = new Car("Car1");
c2 = new Car("Car2");
c3 = new Car("Car3");
c4 = new Car("Car4");
// 세대를 표시한다.
Console.WriteLine("C1 is gen {0} ",
GC.GetGeneration(c1));
Console.WriteLine("C1 is gen {0} ",
GC.GetGeneration(c2));
Console.WriteLine("C1 is gen {0} ",
GC.GetGeneration(c3));
Console.WriteLine("C1 is gen {0} ",
GC.GetGeneration(c4));
// 차 몇대를 직접 제거한다.
c1.Dispose();
c3.Dispose();
// 0 세대를 모두 컬렉트한다.
GC.Collect(0);
// 세대를 다시 출력한다.
Console.WriteLine("C1 is gen {0} ",
GC.GetGeneration(c1));
Console.WriteLine("C1 is gen {0} ",
GC.GetGeneration(c2));
Console.WriteLine("C1 is gen {0} ",
GC.GetGeneration(c3));
Console.WriteLine("C1 is gen {0} ",
GC.GetGeneration(c4));
return 0;
}
}
Output
Attachment:
사용자 지정 열거형 만들기 IEnumerable, IEnumerator
사용자 지정 형식인 Car 객체를 모아 놓은 Cars 라는 이름의 클래스를 만들었다고 가정해 보자.
내부의 Car 들을 얻기 위해서는 Cars 를 반복 처리할 때 foreach 구조를 이용하는 것이 편리할 것이다.
Code:
public class Car
{
private string name;
public Car(string name)
{ this.name = name; }
public string CarName
{ set {name = value; } get { return name; } }
}
public class Cars
{
private Car[] carArray;
public Cars()
{
carArray = new Car[4];
carArray[0] = new Car("Car1");
carArray[1] = new Car("Car2");
carArray[2] = new Car("Car3");
carArray[3] = new Car("Car4");
}
}
public class MainClass
{
public static void Main(string[] args)
{
Cars carLot = new Cars();
foreach( Car c in carLot ) // 컴파일 오류 !!
{
Console.WriteLine("Name: {0}
", c.CarName );
}
}
}
그러나 위 코드는, Cars 클래스에 GetEnumerator() 메소드가 구현되지 않았다는 컴파일 에러가 발생할 것이다.
이 메소드는 System.Collections 네임스페이스에 숨어 있는 IEnumerable 인터페이스에 정의되어 있다.
Code:
// foreach 구문을 이용해서 하위 형식을 얻으려면 컨테이너가 반드시 IEnumerable 을 구현해야 한다.
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
따라서 이 문제를 바로잡기 위해서는 우선 Cars 클래스에 IEnumerable 인터페이스를 추가하고 GetEnumerator() 메소드를 구현해야 한다.
그런데 GetEnumerator() 는 IEnumerator 라는 이름의 또 다른 인터페이스를 반환하게 되어있다.
IEnumerator 는 임의의 객체가 형식들의 내부 컬렉션을 순회하는데 이용할 수 있다.
역시 System.Collections 네임 스페이스에 정의되어 있는 IEnumerator 에는 다음과 같은 세가지 메소드가 정의되어 있다.
Code:
public interface IEnumerator
{
// 컬렉션의 현재 요소를 가져옵니다.
object Current { get; }
// 열거자를 컬렉션의 다음 요소로 이동합니다.
bool MoveNext();
// 컬렉션의 첫 번째 요소 앞의 초기 위치에 열거자를 설정합니다.
void Reset();
}
이제 Cars 클래스는 다음과 같이 수정할 수 있다.
Code:
public class Cars : IEnumerable,
IEnumerator
{
private Car[] carArray;
int pos = -1; // 배열의 현재 위치
public Cars()
{
carArray = new Car[4];
carArray[0] = new Car("Car1");
carArray[1] = new Car("Car2");
carArray[2] = new Car("Car3");
carArray[3] = new Car("Car4");
}
// IEnumerator 인터페이스의 메소드 구현
public bool MoveNext()
{
if (pos < carArray.Length - 1)
{
pos++;
return true;
}
return false;
}
public void Reset()
{ pos = 0; }
public object Current
{ get {return carArray[pos]; } }
// IEnumerable 인터페이스의 메소드 구현
public IEnumerator GetEnumerator()
{
return (IEnumerator)this;
}
}
위의 소스코드는 예를 들어 설명하기 위해서 어쩔 수 없이 모두 구현했지만, 이와 같은 IEumerator 인터페이스 구현에는 실제로는 불필요한 부분이 많다.
배열의 실제 형식인 System.Array 에는 이미 IEnumerator 가 구현되어 있기 대문에, 사실 Car 클래스는 다음과 같이 간략하게 작성될 수 있다.
Code:
public class Cars : IEnumerable //
IEnumerator 는 필요하지 않다.
{
private Car[] carArray;
public Cars()
{
carArray = new Car[4];
carArray[0] = new Car("Car1");
carArray[1] = new Car("Car2");
carArray[2] = new Car("Car3");
carArray[3] = new Car("Car4");
}
// IEnumerable 인터페이스의 메소드 구현
public IEnumerator GetEnumerator()
{
// 이제는 내부 배열로 부터 IEnumerator 를 반환한다.
return carArray.GetEnumerator();
}
}
복사 가능한 객체 만들기 ICloneable
참조 객체의 복사(대입)은 실제 값의 복사가 아닌 단순히 객체의 주소가 복사되는 얕은 복사를 수행한다.
호출자에게 자기 자신의 복사본을 반환할 수 있는 사용자 정의 형식을 만들려면, 표준 ICloneable 인터페이스를 구현하면 된다.
Code:
public interface ICloneable
{
// 현재 인스턴스의 복사본인 새 개체를 만듭니다.
object Clone();
}
IConeable 인터페이스를 사용한 예
Code:
public class Point : ICloneable
{
public int x, y;
public Point() { }
public Point(int x, int y) { this.x = x; this.y = y; }
public override string ToString()
{ return "X: " + x + "Y: " + y; }
public object Clone()
{ return new Point(this.x, this.y); }
}
public class MainClass
{
public static void Main()
{
Point p1 = new Point(50, 50);
Point p2 = (Point)p1.Clone();
}
}
Point 를 위와 같이 구현할 수도 있지만, 좀 더 간단하게 나타낼 수도 있다. Point 형식에 다른 내부 참조 형식에 대한 참조자가 없기 때문에, Clone() 메소드를 다음과 같이 간단하게 할 수도 있다.
( MemberwiseClone() 은 System.Object 에 정의 되어 있는 멤버이다. )
Code:
public object Clone()
{
// Point 의 각 필드를 멤버 대 멤버 복사.
return this.MemberwiseClone();
}
콜벡함수
대부분의 프로그램에서 시스템 내의 객체들은 일반적으로 이벤트, 콜벡 인터페이스와 여타 프로그래밍 구조를 이용해서 '양방향 통신' 에 참여한다.
Windows API 에서는 콜백 함수 또는 줄여서 콜백이라는 이름의 항목을 생성하는 데 C 스타일의 함수 포인터를 이용한다.
프로그래머들은 콜백을 이용해서 하나의 함수가 해당 응용 프로그램에 있는 다른 함수에게 보고(콜벡)하도록 구성할 수 있었다.
포준 C 스타일 함수는 메모리에 있는 주소를 가리키는 것에 지나지 않는다는 데에 문제가 있다.
이상적으로는 콜백에 매개변수의 개수나 형식 그리고 지시된 메소드의 반환 값과 같은 형식 안전 정보가 포함되는 것이 바람직하다.
아쉽게도, 전통적인 콜백 함수에서는 이것이 불가능하기 때문에, 이로 인해 버그나 심각한 충돌 등 여러 런타임 에러가 종종 발생하곤 한다.
델리게이트 Delegate
.NET 에서는 델리게이트를 이용해서 콜벡 기능을 더 안정적이고 더 객체 지향적인 방식으로 수행할 수 있다.
본질적으로, 델리게이트는 해당 응용 프로그램 내에 있는 다른 메소드를 가리키는 객체이다.
.NET Framework 에서는 동기 델리게이트와 비동기 델리게이트를 이용할 수 있다.
구체적으로, 델리게이트에는 세가지 중요한 정보가 포함된다.
- 델리게이트가 호출하는 메소드의 이름
- 이 메소드의 인수
- 이 메소드의 반환 값
* 델리게이트를 설명하는 과정이 다소 복잡해 보이지만, 델리게이트는 함수 포인터를 캡슐화하는 하나의 래퍼 클래스에 지나지 않는다.
가장 간단한 델리게이트 예제예제1
주어진 델리게이트의 대상(즉, 호출할 메소드)을 할당하고 싶으면, 해당 메소등의 이름을 델리게이트의 생성자로 전달하면 된다.
이렇게 함으로써, 함수를 직접 호출하는 것처럼 보이는 구문을 이용해서 해당 멤버를 간접적으로 호출할 수 있게 된다.
Code:
using System;
class Program
{
// 이 메소드는 델리게이트에 의해 호출될 메소드이다.
public static void PlainPrint(string msg)
{ Console.WriteLine("Msg is : {0} ", msg); }
// 델리게이트 하나를 정의한다.
public delegate void AnyMethodTakingAString(string s);
// 메인함수.
public static void Main(string[] args)
{
// 델리게이트를 만든다.
AnyMethodTakingAString del;
del = new AnyMethodTakingAString(PlainPrint);
// 내부적으로
AnyMethodTakingAString.Invoke() 가 여기에서 호출된다 !!
del("Hello there...");
// 델리게이트에 대한 정보를 출력한다.
Console.WriteLine("I just called : {0} ",
del.Method);
}
}
Output
Attachment:
예제 2
델리게이트 객체는 그것이 호출하는 메소드의 실제 이름과는 크게 상관이 없다.
원한다면 다음과 같이 대상 메소드를 동적으로 변경할 수도 있다.
Code:
using System;
class Program
{
public static void PlainPrint(string msg)
{ Console.WriteLine("Msg is : {0} ", msg); }
public static void UpperCasePrint(string msg)
{ Console.WriteLine("Msg is : {0} ", msg.ToUpper()); }
public delegate void AnyMethodTakingAString(string s);
public static void Main(string[] args)
{
AnyMethodTakingAString del;
del = new AnyMethodTakingAString(PlainPrint);
del("Hello there...");
Console.WriteLine("I just called :
{0}\n", del.Method);
// 델리게이트를 재할당하고 호출한다.
del = new AnyMethodTakingAString(UpperCasePrint);
del("Hello there...");
Console.WriteLine("I just called :
{0}\n", del.Method);
}
}
Output
Attachment:
예제3
System.Delegate 기본 클래스에 포함된 호출 리스트에 여러 개의 메소드를 삽입하려면, 오버로드된 += 연산자를 이용하면 된다.
Code:
using System;
class Program
{
public static void PlainPrint(string msg)
{
Console.WriteLine("Msg is : {0} ",
msg);
}
public static void UpperCasePrint(string msg)
{
Console.WriteLine("Msg is : {0} ",
msg.ToUpper());
}
public delegate void AnyMethodTakingAString(string s);
public static void Main(string[] args)
{
// 멀티 캐스팅
AnyMethodTakingAString del;
del = new AnyMethodTakingAString(PlainPrint);
del += new AnyMethodTakingAString(UpperCasePrint);
del("Hello there...");
}
}
Output
Attachment:
좀 더 복잡한 델리게이트 예제
현재, 위에서 만든 델리게이트는 논리적으로 관련된 클래스 형식으로부터 분리되어 있다.
이렇게 해도 문제는 없지만, 델리게이트를 클래스 내에서 직접 정의 하는 방법이 좀 더 현명한 방법니다.
Code:
using System;
using System.Collections;
public class Car
{
private string name;
private bool isDirty;
private bool shouldRotate;
public delegate void CarDelegate(Car c);
public Car(string name, bool dirty, bool rotate)
{
this.name = name;
isDirty = dirty;
shouldRotate = rotate;
}
public bool Dirty
{
get { return isDirty; }
set { isDirty = value; }
}
public bool Rotate
{
get { return shouldRotate; }
set { shouldRotate = value; }
}
}
public class Garage
{
ArrayList theCars = new ArrayList();
public Garage()
{
theCars.Add(new Car("Viper", true,
false));
theCars.Add(new Car("Fred", false,
false));
theCars.Add(new Car("Stan", false,
true));
}
public void ProcessCar(Car.CarDelegate proc)
{
Console.WriteLine("*** Calling: {0} ***",
proc.Method.ToString());
// 인스턴스 메소드를 호출하는가 아니면 정적메소드를 호출하는가 ?
if (proc.Target != null)
Console.WriteLine("-->
Target: {0}", proc.Target.ToString());
else
Console.WriteLine("--> Target
is a static method");
// 메소드를 호출하면서 각 차를 전달한다.
foreach (Car c in theCars)
proc(c);
Console.WriteLine();
}
}
public class CarApp
{
// 델리게이트의 대상.
public static void WashCar(Car c)
{
if (c.Dirty)
Console.WriteLine("Cleaning a
car");
else
Console.WriteLine("This car is
already clean...");
}
// 델리게이트의 또다른 대상.
public static void RotateTires(Car c)
{
if (c.Rotate)
Console.WriteLine("Tires have
been rotated");
else
Console.WriteLine("Don't need to
be rotated...");
}
public static int Main(string[] args)
{
// 정비 공장을 만든다.
Garage g = new Garage();
// 더러운 차를 세차한다.
g.ProcessCar(new Car.CarDelegate(WashCar));
// 타이어를 교체한다.
g.ProcessCar(new Car.CarDelegate(RotateTires));
return 0;
}
}
Output
Attachment:
멀티캐스트 델리게이트를 이용하면 여러 델리게이트를 한번에 호출할 수 있다.
멀티캐스트 델리게이트는 여러가지 방법으로 구현할 수 있다.
Code:
public static int Main(string[] args)
{
// 정비 공장을 만든다.
Garage g = new Garage();
// 두 개의 델리게이트를 새로 정의한다.
Car.CarDelegate wash = new Car.CarDelegate(WashCar);
Car.CarDelegate rotate = new Car.CarDelegate(RotateTires);
// 멀티캐스트 델리게이트에 오버로드된 + 연산자를 이용할 수 있다.
g.ProcessCar(wash + rotate);
return 0;
}
+ 연산자는 정적 Delegate.Combine() 메소드를 호출하는 약어 표시이다.
Code:
// + 연산자는
Combie() 메소드와 동일한 효과를 발휘한다.
g.ProcessCar((Car.CarDelegate)Delegate.Combine(wash + rotate));
이 새 델리게이트를 나중에도 사용하려면 다음과 같이 작성할 수도 있다.
Code:
// 두 개의 델리게이트를 새로 정의한다.
Car.CarDelegate wash = new Car.CarDelegate(WashCar);
Car.CarDelegate rotate = new Car.CarDelegate(RotateTires);
// 이 새로운 델리게이트를 나중에 사용하기 위해서 저장해 둔다.
MulticastDelegate d = wash + rotate;
// 이 새로운 델리게이트를
ProcessCars() 메소드로 보낸다.
g.ProcessCar((Car.CarDelegate)d);
멀티캐스트 델리게이트를 목록에서 항목을 제거하고 싶으면, Remove() 메소드를 호출하면 된다.
Code:
// 이 정적 Remove() 메소드는 Delegate 형식을 반환한다.
Delegate washOnly = MulticastDelegate.Remove(d, rotate);
g.ProcessCars((Car.CarDelegate)washOnly);
비동기적 델리게이트
델리게이트 함수 호출을 멀티스레딩으로 메인 스레드와 별도의 스레드에서 실행되게 할 수도 있다.
C# 컴파일러가
'delegate' 키워드를 처리할 때, BeginInvoke() 와 EndInvoke() 라는 이름의 두 메소드가 동적으로 생성되는데 비동기적으로 호출할때 사용된다.
동기 델리게이트의 호출
Code:
using System;
using System.Threading;
public class CarApp
{
public static void MyPrint(string msg)
{
Console.WriteLine("MyPrint() is on thread
{0}", Thread.CurrentThread.GetHashCode());
Thread.Sleep(10000);
Console.WriteLine("msg : {0}", msg);
}
public delegate void NewDelegate(string msg);
public static int Main(string[] args)
{
Console.WriteLine("Main() is on thread
{0}", Thread.CurrentThread.GetHashCode());
NewDelegate d = new NewDelegate(MyPrint);
d("Your function is ready !");
Console.WriteLine("Done invoking
delegate");
return 0;
}
}
Output
Attachment:
메소드를 비동기적으로 호출하기.
Code:
using System;
using System.Threading;
public class CarApp
{
public static void MyPrint(string msg)
{
Console.WriteLine("MyPrint() is on thread
{0}", Thread.CurrentThread.GetHashCode());
Thread.Sleep(10000);
Console.WriteLine("msg : {0}", msg);
}
public delegate void NewDelegate(string msg);
public static int Main(string[] args)
{
Console.WriteLine("Main() is on thread
{0}", Thread.CurrentThread.GetHashCode());
NewDelegate d = new NewDelegate(MyPrint);
IAsyncResult itfAR = d.BeginInvoke("Your
function is ready !", null, null);
Console.WriteLine("Done invoking
delegate");
// 다른 작업을 한다...
d.EndInvoke(itfAR);
return 0;
}
}
Output
Attachment:
비동기적 델리게이트로서의 콜벡
Code:
using System;
using System.Threading;
using System.Runtime.Remoting.Messaging;
public class CarApp
{
// 델리게이트 함수 호출이 완료되었을때 호출될 함수.
public static void MyCallBack(IAsyncResult itfAR)
{
Console.WriteLine("DelegateCallBack on thread
{0}", Thread.CurrentThread.GetHashCode());
AsyncResult res = (AsyncResult)itfAR;
NewDelegate d = (NewDelegate)res.AsyncDelegate;
d.EndInvoke(itfAR);
}
public static void MyPrint(string msg)
{
Console.WriteLine("MyPrint() is on thread
{0}", Thread.CurrentThread.GetHashCode());
Thread.Sleep(10000);
Console.WriteLine("msg : {0}", msg);
}
public delegate void NewDelegate(string msg);
public static int Main(string[] args)
{
Console.WriteLine("Main() is on thread
{0}", Thread.CurrentThread.GetHashCode());
NewDelegate d = new NewDelegate(MyPrint);
d.BeginInvoke("Your function is ready !",
new AsyncCallback(MyCallBack), null);
Console.WriteLine("Done invoking
delegate");
Console.ReadLine(); // 호출을 마칠때까지 콘솔이 떠 있게 하기 위해.
return 0;
}
}
Output
Attachment:
C# 의 동기화동기화를 하지 않은 소스 예제
Code:
using System;
using System.Threading;
internal class WorkerClass
{
private int theInt;
public void DoSomeWork()
{
theInt++;
for (int i = 0; i < 3; i++)
{
Console.WriteLine("theInt: {0},
i: {1}, current thread: {2}",
theInt, i,
Thread.CurrentThread.Name);
Thread.Sleep(1000);
}
}
}
public class App
{
public static void Main()
{
// 단일 작업 객체를 만든다.
WorkerClass w = new WorkerClass();
// 세개의 부 스레드를 만들어 각 스레드가 동일한 공유객체를 호출하게 만든다.
Thread threadA = new Thread(new
ThreadStart(w.DoSomeWork));
threadA.Name = "A";
Thread threadB = new Thread(new
ThreadStart(w.DoSomeWork));
threadB.Name = "B";
Thread threadC = new Thread(new
ThreadStart(w.DoSomeWork));
threadC.Name = "C";
// 하나씩 시작시킨다.
threadA.Start();
threadB.Start();
threadC.Start();
}
}
Output
Attachment:
lock 키워드를 이용한 동기화
'lock' 키워드를 이용하면 코드 블록을 잠가서 현재 스레드가 작업을 완전히 끝낼 때까지 다른 스레드들이 기다리게 만들 수 있다.
'lock' 키워드를 이용하려면 lock 수행문의 범위로 진입하기 위해 스레드가 얻어야 하는 토큰(객체 참조자)을 전달해야 한다.
인스턴스 레벨 메소드를 잠그려면, 현재 형식에 대한 참조자를 전달하기만 하면 된다.
Code:
internal class WorkerClass
{
private int theInt;
public void DoSomeWork()
{
lock (this)
{
theInt++;
for (int i = 0; i < 3; i++)
{
Console.WriteLine("theInt: {0}, i: {1}, current thread: {2}",
theInt,
i, Thread.CurrentThread.Name);
Thread.Sleep(1000);
}
}
}
}
Output
Attachment:
System.Threading.Monitor 형식을 이용한 동기화
C# 의 lock 수행문은 System.Threading.Monitor 클래스 형식을 이용한 작업을 간소화한 것이다.
Monitor 형식을 이용하면 실행 스레드가 일정 시간 동안 기다리게 하거나(Wait() 메소드를 통해서), 현재 스레드가 완료되었을 때 기다리고 있는 스레드들에게 알려주도록할 수 있다.
내부적으로, 앞의 잠금 로직은 실제로 다음과 같이 처리된다.
Code:
internal class WorkerClass
{
private int theInt;
public void DoSomeWork()
{
// 모니터에 토큰과 함께 진입한다.
Monitor.Enter(this);
try
{
theInt++;
for (int i = 0; i < 3; i++)
{
Console.WriteLine("theInt: {0}, i: {1}, current thread: {2}",
theInt,
i, Thread.CurrentThread.Name);
Thread.Sleep(1000);
}
}
finally
{
// 모니터를 나가서 토큰을 해제해야 한다.
Monitor.Exit(this);
}
}
}
System.Threading.Interlocked 형식을 이용한 동기화
System.Threading 네임스페이스는 단일 데이터 포인트에서 원자적으로 연산하는 데 이용할 수 있는 형식을 제공한다.
Interlocked 형식의 멤버
- Increment() : 값을 1만큼 안전하게 증가시킨다.
- Decrement() : 값을 1만큼 안전하게 감소시킨다.
- Exchange() : 두 값을 안전하게 교환한다.
- CompareExchange() : 두 값이 같은지 안전하게 검사하고, 같으면 하나의 값을 세 번째 값으로 바꾼다.
원자적으로 단일 값을 변경하는 과정은 다중 스레드 환경에서 매우 일반적이다. 따라서 동기화 코드를 lock 을 이용해서 작성하기 보다는 Interlocked 형식을 사용하여 작성하는 것이 좋다.
Code:
int i=0;
lock(this)
{ i++; }
Code:
// 변경하고자 하는 값을 참조로 전달한다.
int i=0;
Interlocked.Increment(ref i);
Code:
int i=9;
Interlocked.Exchange(ref i, 83);
Code:
// i의 값이 현재 83 이면, i를 99로 바꾼다.
Interlocked.CompareExchange(ref i, 83, 99);
Timer Callback 을 이용한 프로그래밍
응용 프로그램에서 일정한 간격을 두고 특정 메소드를 호출해야 하는 경우가 종종 있다. 예를 들어, 도우미 함수를 통해 현재 시간을 상태 바에 표시하는 응용 프로그램이 있을 수 있다. 도 다른 예로, 이메일 메시지를 확인하는 백그라운드 작업을 수행하기 위해서 수시로 도우미 함수를 호출하는 응용 프로그램을 만들 수도 있을 것이다.
이러한 경우, System.Threading.Timer 형식을 TimerCallback 이라는 이름의 관련 델리게이트와 함께 이용할 수 있다.
델리게이트 대상이 사용할 정보를 보내고 싶으면, 생성자의 두번째 매개변수에 있는 null 값을 적당한 정보로 바꾸면 된다.
Code:
using System;
using System.Threading;
public class App
{
static void PrintTime(object state)
{
Console.WriteLine("Time is: {0}",
DateTime.Now.ToLongTimeString());
}
public static void Main()
{
TimerCallback timeCB = new
TimerCallback(PrintTime);
Timer t = new Timer(
timeCB, // TimerCallback 델리게이트 형식.
null, // 호출된 메소드로 전달될 정보 (정보가 없으면
null)
0, // 시작하기 전에 기다릴 시간.
1000); // 호출 사이의 간격
Console.WriteLine("Hit key yo
terminate...");
Console.ReadLine();
}
}
System.Type 클래스
System.Type 클래스에는 현재 관찰 중인 형식에 대한 중요한 정보를 추출해 내는데 이용할 수 있는 여러 가지 메소드들이 포함되어 있다.
System.Type 의 멤버
- IsAbstract
/ IsArray / IsClass / IsCOMObject / IsEnum / IsInterface / IsPrimitive /
IsNestedPublic / IsNestedPrivate / IsSealed / IsValueType
: 이 속성들을 이용하면 참조하고 있는 Type 에 대한 여러 가지 기본적인 정보를 알아낼 수 있다. (즉, 추상 메소드인지, 배열인지, 중첩 클래스인지 등) - GetConstructors()
/ GetEvents() / GetFields() / GetInterfaces() / GetMombers() /
GetNestedTypes() / GetProperties()
: 이 메소드들을 이용하면 알아내고 싶은 항목들(인터페이스, 메소드, 속성 등)을 나타내는 배열을 얻을 수 있다. 각 메소드들은 관련된 배열을 반환한다 - FindMembers() : 검색 기준에 기반해서, MemberInfo 형식의 배열을 반환한다.
- GetType() : 이 정적 메소드는 해당 문자열 이름에 대한 Type 인스턴스를 반환한다.
- InvokeMember() : 이 메소드를 이용하면 해당 아이템에서 late 바인딩을 이용할 수 있다.
형식 참조자 얻기
1. System.Object 에 정의된 메소드로서 Type 클래스의 인스턴스를 반환하는 GetType() 이라는 메소드를 이용할 수 있다.
Code:
// 유효한
Foo 인스턴스를 이용해서 Type 을 추출한다.
Foo theFoo = new Foo();
Type t = theFoo.GetType();
2. Type 클래스 자체를 이용해서 정적 멤버인 GetType() 을 호출하고 얻어내고자 하는 항목의 이름을 지정할 수 있다.
Code:
// 정적 메소드인 Type.GetType() 을 이용해서 Type 을 가져온다.
Type t = null;
t = Type.GetType("Foo");
// 별도의 어셈블리에 있는 중첩 형식을 가져온다.
t = Type.GetType("MyNamespace.OuterType+NestedType, myOtherAsm");
3. C# 의 typeof() 연산자를 이용해서 Type 의 인스턴스를 얻을 수도 있다.
Code:
// typeof 를 이용해서 Type 을 가져온다.
Type t = typeof(Foo);
Type 클래스 활용 예제
Code:
using System;
using System.Reflection;
namespace TheType
{
// 두 개의 인터페이스
public interface IFaceOne
{ void MethodA(); }
public interface IFaceTwe
{ void MethodB(); }
// Foo 는 이 두 인터페이스를 지원한다.
public class Foo : IFaceOne, IFaceTwe
{
// 필드
public int MyIntField;
public string myStringField;
// 메소드
public void myMethod(int p1, string p2) { }
// 속성
public int MyProp
{
get { return MyIntField; }
set { MyIntField = value; }
}
// 인터페이스
public void MethodA() { }
public void MethodB() { }
}
public class App
{
public static void Main()
{
Foo f = new Foo();
Type t = f.GetType();
Console.WriteLine("***** Various
stats about Foo *****");
Console.WriteLine("Full name is:
{0}", t.FullName);
Console.WriteLine("Base is:
{0}", t.BaseType);
Console.WriteLine("Is it
abstract? {0}", t.IsAbstract);
Console.WriteLine("Is it a COM
object? {0}", t.IsCOMObject);
Console.WriteLine("Is it sealed?
{0}", t.IsSealed);
Console.WriteLine("Is it a
class? {0}", t.IsClass);
Console.WriteLine("*****
Properties of Foo *****");
PropertyInfo[] pi =
t.GetProperties();
foreach (PropertyInfo prop in pi)
Console.WriteLine("Prop: {0}", prop.Name);
Console.WriteLine("***** Methods
of Foo *****");
MethodInfo[] mi = t.GetMethods();
foreach (MethodInfo m in mi)
Console.WriteLine("Method: {0}", m.Name);
Console.WriteLine("***** Fields
of Foo *****");
FieldInfo[] fi = t.GetFields();
foreach (FieldInfo field in fi)
Console.WriteLine("Field: {0}", field.Name);
Console.WriteLine("*****
Interfaces of Foo *****");
Type[] ifaces = t.GetInterfaces();
foreach (Type i in ifaces)
Console.WriteLine("Interface: {0}", i.Name);
Console.WriteLine("**************************");
}
}
}
Output
Attachment: