textBoxCity.DataBindings.Add("Text", address, "City");이 코드는 textBoxCity 컨트롤의 Text 프로퍼티와 address 객체의 City 프로퍼티를 바인딩한 다.[ITEM #38 참고] 이 코드는 City라는 이름의 public 멤버 변수에 대해서는 동작하지 않는다. public 데이터 멤버를 사용하는 것은 좋은 방법이 아니기 때문에 프레임워크 클래스 라이브러 리 설계자는 이러한 방법을 제공하지 않았다. 그들은 우리가 좀 더 나은 객체지향적 기술을 사용 하기 바랬기 때문이다. C++나 자바 개발자들은 불만이 있을 수도 있겠지만, 데이터 바인딩 코드 는 get/set류의 메서드 어떤 것도 찾지 않는다. get/set류의 메서드 대신 프로퍼티를 사용하자.
public class Customer { private string _name; public string Name { get { return _name; } set { if ((value == null) || (value.Length == 0)) throw new ArgumentException("Name cannot be blank", "Name"); _name = value; } } }만일 public 데이터 멤버 형태로 고객의 이름을 저장하고 있었다면 코드 전체에서 customer 이름을 변경하는 부분을 모두 찾아내어 수정해야 하는데, 이는 많은 시간이 소모된다.
public string Name { get { lock(this) { return _name; } } set { lock(this) { _name = value; } } }프로퍼티는 메서드의 언어적인 특성을 모두 가지고 있다. 따라서 프로퍼티는 virtual로 지정이 가능하다.
public class Customer { private string _name; public virtual string Name { get { return _name; } set { _name = value; } } }또한, 프로퍼티는 abstract 형태로 정의되거나 interface의 한 부분으로 정의될 수도 있다.
public interface INameValuePair { object Name { get; } object Value { get; set; } }마지막으로, interface를 const와 non-const 형태로 만들 수 있다.
public interface INameValuePair { object Name { get; } object Value { get; set; } } public interface INameValuePair { object Value { get; set; } } // 사용예: public class Stuff : IConstNameValuePair, INameValuePair { private string _name; private object _value; #region IConstNameValuePair Memebers public object Name { get { return _name; } } object IConstnameValuePair.Value { get { return _value; } } #endregion #region INameValuePair Memebers public object Value { get { return _value; } set { _value = value; } } #endregion }프로퍼티는 클래스 내부의 데이터를 가져오거나 수정할 수 있는 메서드의 확장으로 볼 수도 있 다. 따라서 메서드를 통해서 할 수 있는 모든 동작은 프로퍼티에서도 똑같이 수행 가능하다.
public class Customer { private string _name; public virtual string Name { get { return _name; } protected set { _name = value; } } }프로퍼티 문법은 단순 데이터 필드를 좀 더 확장해서 사용할 수 있도록 하는데, 특정 타입을 사 용할 때 배열과 같이 인덱스를 이용한다. 인덱서(indexer)라고도 하는 이 기능은 배열의 인덱스 를 넘겨받는 프로퍼티라고도 볼 수 있다. 이러한 기능은 하나의 프로퍼티를 이용하여 다수의 값 에 접근하고자 할 때 매우 유용하다.
public int this [int index] { get { return _theValues[index]; } set { _theValues[index] = value; } } int val = MyObject[i];인덱서는 하나의 값에 접근하는 프로퍼티와 동일한 특성을 갖는다. 인덱서는 메서드의 형태로 구현되고, 인덱서 내부에 값에 대한 검증이나 특정 계산 루틴을 포함시킬 수 있다. 또한 virtual 이나 abstract 형태로 만들어서 interface 내부에 선언하거나, 읽기 전용 또는 읽고 쓰기 가능 한 형태로 만들 수 있다. 숫자 인덱스를 사용하는 1차원 배열 형태의 인덱서는 데이터 바인딩에 서도 사용된다. 숫자가 아닌 인덱스를 사용할 경우에는 맵(map)이나 디렉토리(directory)와 같 은 자료구조의 표현도 가능하다.
public Address this[string name] { get { return _theValues[name]; } set { _theValues[name] = value; } }C# 언어에서의 다차원 배열 형태로 인덱서를 구성할 수도 있는데, 이 경우 각각의 차원에 대해 서 서로 다른 인덱스형을 지정할 수도 있다.
public int this[int x, int y] { get { return ComputeValue(x, y); } } public int this[int x, string name] { get { return ComputeValue(x, name); } }인덱서는 임의로 이름을 지정할 수 없기 때문에 항상 this 키워드를 이용하여 선언된다. 그러므 로 모든 타입은 항상 한 개의 인덱서만을 포함시킬 수 있다.
public class Customer { public string Name; }이 클래스는 고객의 이름을 포함하는 Customer 클래스다. 고객의 이름을 얻거나 변경하기 위 해서 다음과 같은 코드를 사용한다.
string name = customerOne.Name; customerOne.Name = "This Company, Inc";이것은 매우 간단하고 수월해 보인다. 아마도 이러한 코드를 작성한 개발자는 나중에 Name이 라는 public 멤버를 프로퍼티로 변경하면 이를 사용하는 코드는 아무런 변경 없이 동작이 가능 하다고 생각했을 것이다. 일면 맞는 말이다.
.field public string NameName 필드로부터 값을 가져오는 코드는 다음과 같은 IL 코드를 만들어낸다.
ldloc.0 ldfld string NameSpace.Customer::Name stloc.1Name 필드에 값을 저장하는 코드는 다음과 같은 IL 코드를 만들어낸다.
ldloc.0 ldstr "This Company, Inc" stfld string NameSpace.Customer::NameIL을 하루 종일 볼 것은 아니기 때문에 IL을 모른다고 너무 걱정하지 말자. 여기서는 데이터 멤 버에 접근하는 코드와 프로퍼티에 접근할 때의 코드가 서로 다르고, 이러한 차이점이 결국 이진 호환성을 깨뜨린다는 것만 알면 된다. 이번에는 프로퍼티를 사용하는 Customer 타입을 보자.
public class Customer { private string _name; public string Name { get { return _name; } set { _name = value; } } }이 Customer 타입을 사용하는 코드는 앞에서 알아본 것과 완전히 동일하다.
string name = customerOne.Name; customerOne.Name = "This Company, Inc";하지만 C# 컴파일러가 생성한 IL 코드는 완벽히 다르다. 다음에 나타난 Customer 타입에 대한 IL 코드를 보자.
.property instance string Name( ) { .get instance string Customer::get_Name( ) .set instance void Customer::set_Name(string) } // Customer::Name 메서드의 끝 .method public hidebysig specialname instance string get_Name( ) cil managed { // 코드 크기 12 (0xc) .maxstack 1 .locals init ([0] string CS$1$0000) IL_0000: nop IL_0001: ldarg.0 IL_0002: ldfld string Customer::_name IL_0007: stloc.0 IL_0008: br.s IL_000a IL_000a: ldloc.0 IL_000b: ret } // Customer::get_Name 메서드의 끝 .method public hidebysig specialname instance void set_Name(string "value") cil managed { // 코드 크기 9 (0x9) .maxstack 8 IL_0000: nop IL_0001: ldarg.0 IL_0002: ldarg.1 IL_0003: stfld string Customer::_name IL_0008: ret } // Customer::set_Name 메서드의 끝가장 중요한 점은 IL 코드내에서 프로퍼티가 어떻게 표현되는가 하는 문제이다. 위에서 보듯이 .property 지시자에 의해서 프로퍼티가 선언된다. 그리고 이 프로퍼티에 접근할 수 있는 get/set accessor가 구현된다. 두 개의 메서드는 사용자가 직접적으로 메서드를 사용할 수 없 도록 hidebysig와 specialname으로 선언되어 있다. 직접적으로 이 메서드를 호출하는 코드 는 쓸 수 없지만 우리는 프로퍼티 접근 방식으로 이러한 메서드를 간접적으로 사용할 수 있다.
// get IL_0007: ldloc.0 IL_0008: callvirt instance string Customer::get_Name( ) IL_000d: stloc.1 // set IL_000e: ldloc.0 IL_000f: ldstr "This Company, Inc" IL_0014: callvirt instance void Customer::set_Name(string)설사 우리가 동일한 형태의 코드를 사용해서 Customer 타입에 접근할지라도 Name이 데이터 멤버로 구현되었는지 아니면 프로퍼티로 구현되었는지에 따라 서로 다른 코드가 만들어진다. 프로퍼티나 데이터 멤버에 접근하는 코드를 만드는 것은 C# 컴파일러의 몫이므로 우리가 제어 할 수 있는 방법은 없다.
최신 콘텐츠