Skip to main content

[This is C#] Chapter 9. 프로퍼티

이것이 C#이다 책을 읽고 공부한 노트입니다.




public 필드의 유혹 #

  • 은닉성을 위해서 필드를 private 으로 선언했다. 어떻게 편리하게 접근하고 값을 변경할 수 있을까?



메소드보다 프로퍼티 #

  • getter와 setter를 만드는 대신 프로퍼티를 사용해볼 수 있겠다.
  • 프로퍼티(Property)
    • get{}, set{}을 일컬어 접근자(Accessor)라고 한다.
    • 쓰기 전용 프로퍼티 → set{} 접근자를 구현하지 않으면 되겠다.
    • 읽기 전용 프로퍼티 → get{} 접근자를 구현하지 않으면 되겠다.
class MyClass
{
    private int num;           // 필드

    public int Num             // 프로퍼티
    {
        get { return num; }    // get 접근자
        set { num = value; }   // set 접근자
    }
}

class MainApp
{
    static void Main(string[] args)
    {
        MyClass a = new MyClass();

        int num = a.Num;  // get 접근자로 읽기 가능
        a.Num = 1;        // set 접근자로 쓰기 가능
    }
}



자동 구현 프로퍼티 #

  • 자동 구현 프로퍼티(Auto-Implemented Property)
    • get{} , set{} 접근자에 조건이 없는 단순한 경우 좀 더 간결하게 코드를 작성할 수 있는 방법이다.

  • 다음과 같은 코드는 위의 코드와 같은 코드가 된다.
class MyClass
{
    public int Num { get; set; }  // 자동 구현 프로퍼티 
}

  • 자동구현 프로퍼티는 선언과 동시화 초기화를 할 수 있다.
class MyClass
{
    public int Num { get; set; } = 29; // 선언과 동시에 초기화 가능
}



프로퍼티로 초기화하기 #

  • 객체를 생성할 때 생성자에 매개변수를 넘기는 대신, 프로퍼티를 사용해서 각 필드를 초기화할 수 있다.
class MyClass
{
    public int Age { get; set; }
}

class MainApp
{
    static void Main(string[] args)
    {
        MyClass a = new MyClass()  // 프로퍼티를 사용해 초기화할 수 있다 
        {
            Age = 1
        };
    }
}



읽기 전용 프로퍼티 #

  • (1) 초기화 전용(Init-Only) 자동 구현 프로퍼티 (C# 9.0)
    • init 접근자를 사용한다.
    • 객체를 초기화할 때 한 번 초기화가 이루어진 이후에는 변경할 수 없는 읽기 전용 프로퍼티이다.
class MyClass
{
    public string Name { get; init; } // init 접근자 사용
}

class MainApp
{
    static void Main(string[] args)
    {
        MyClass a = new MyClass() // 초기화를 하였다. 
        {
            Name = "Kim"
        };

        a.Name = "Lee"; // (X) 초기화 후에는 변경이 불가능하다.
    }
}

  • (2) set{} 접근자를 지정하지 않는다.
    • 생성자에서만 초기화가 가능하다. init{} 접근자의 경우처럼 프로퍼티를 사용해서 초기화할 수 없다.
class MyClass
{
    public string Name { get; }  // set 접근자가 없는 읽기 전용 프로퍼티는

    public MyClass()
    {
        Name = "Kim";  // 생성자에서만 초기화할 수 있다. 
    }
}

class MainApp
{
    static void Main(string[] args)
    {
        MyClass a = new MyClass() 
        {
            Name = "Kim"  // (X) 프로퍼티를 사용해 초기화하는 것이 불가능하다 
        };
    }
}

  • (3) private set{}
    • 클래스 내 다른 메서드에서 값을 변경할 수 있다.
class MyClass
{
    public string Name { get; private set; }  // private set인 읽기 전용 프로퍼티
        
    public MyClass()
    {
        Name = "Kim";
    }

    public void ChangeName()
    {
        Name = "Lee";  // 클래스 내 다른 메서드에서 값을 변경할 수 있다. 
    }
}

class MainApp
{
    static void Main(string[] args)
    {
        MyClass a = new MyClass();
    }
}



읽기 전용 Collection 프로퍼티 #

  • 컬렉션의 경우에는 어떻게 읽기 전용 프로퍼티를 만들어야 할까?
class Mine
{
    List<int> keys = new List<int>();
    public List<int> Keys => keys; // 이렇게하면
}

class Main
{
    public void Run()
    {
        Mine m = new Mine();
        m.Keys.Add(1); // Keys에 요소를 추가 삭제할 수 있다는 문제가 있다. 
    }
}
class Mine
{
    List<int> keys = new List<int>();
    public IEnumerable<int> Keys => keys; // 리턴 타입을 읽기 전용인 IEnumerable로 변경하면
}

class Main
{
    public void Run()
    {
        Mine m = new Mine();
        m.Keys.Add(1); // 컴파일 에러! Keys에 요소를 추가 삭제할 수 없다.
        int val = m.Keys[1]; // 하지만 이것도 안 된다는 문제가 있다. 
    }
}
class Mine
{
    List<int> keys = new List<int>();
    public ReadOnlyCollection<int> Keys => new ReadOnlyCollection<int>(keys);
    // 그래서 이렇게 사용하면
}

class Main
{
    public void Run()
    {
        Mine m = new Mine();
        m.Keys.Add(1); // 컴파일 에러
        int val = m.Keys[1]; // 이것은 접근이 가능하다
    }
}



레코드 형식으로 만드는 불변 객체 #

  • 레코드(Record)
    • 데이터 복사와 비교를 손쉽게 할 수 있는 불변 객체이다.
    • 참조 형식이다.

레코드 선언하기 #

  • 레코드 선언하기
    • record키워드와 초기화 전용 자동 구현 프로퍼티를 함께 이용해서 선언한다. 쓰기 기능한 프로퍼티와 필드도 넣을 수 있다.
record RTransaction
{
    public string From { get; init; }
    public string To { get; init; }
    public int Amount { get; init; }
}

class MainApp
{
    static void Main(string[] args)
    {
        RTransaction r1 = new RTransaction { From="Alice", To="Bob", Amount=100 };
        RTransaction r2 = new RTransaction { From="John", To="Max", Amount=200 };
    }
}

with을 이용한 레코드 복사 #

record RTransaction
{
    public string From { get; set; }
    public string To { get; set; }
    public int Amount { get; set; }
}

class MainApp
{
    static void Main(string[] args)
    {
        RTransaction r1 = new RTransaction { From="Alice", To="Bob", Amount=100 };
        RTransaction r2 = r1 with { To="Max" }; // To만 Max로 바꾸면서 r1을 r2에 복사한다.
    }
}

레코드 객체 비교하기 #

  • 컴파일러는 레코드의 상태를 비교하는 Equals() 메소드를 자동으로 구현한다.
record RTransaction
{
    public string From { get; set; }
    public string To { get; set; }
    public int Amount { get; set; }
}

class MainApp
{
    static void Main(string[] args)
    {
        RTransaction r1 = new RTransaction { From="Alice", To="Bob", Amount=100 };
        RTransaction r2 = r1 with { To="Max" };

        Console.WriteLine($"{r1.Equals(r2)}"); // False
    }
}



무명 형식 #

  • 무명 형식(Anonymous Type)
    • int, MyClass같은 형식 이름이 없는 것이다.
    • 괄호 사이에 임의의 프로퍼티 이름을 적고, 값을 할당하면 무명 형식의 프로퍼티가 만들어진다.
    • 선언과 동시에 인스턴스를 할당한다.
var a = new { Name = "Kim", Age = 1 };  // 선언과 동시에 인스턴스 할당 

  • 인스턴스가 한 번 만들어지고 난 후에는 변경이 불가능하다. 즉, 읽기만 가능하다.
var a = new { Name = "Kim", Age = 1 }; 

string name = a.Name;  // 프로퍼티애 접근해서 읽기 가능
int age = a.Age;

a.Name = "Lee";        // (X) 인스턴스가 한 번 만들어진 후에는 변경이 불가능하다 



인터페이스의 프로퍼티 #

  • 인터페이스의 프로퍼티
    • 당연히 인터페이스의 프로퍼티는 구현을 갖지 않는다.
    • 그래서 클래스의 자동 구현 프로퍼티 선언과 그 모습이 동일하다.
interface IMyInterface
{
    string Name { get; }
}

class MyClass : IMyInterface
{
    private string name;
    public string Name
    {
        get { return name; }
    }
}



추상 클래스의 추상 프로퍼티 #

  • 추상 프로퍼티는 어떻게 선언할 수 있을까?
    • 인터페이스처럼 구현을 비워놓을 수는 없다. 그러면 C# 컴파일러가 자동 구현 프로퍼티로 간주하고 구현을 자동으로 채워 넣을 것이다.
    • 따라서 abstract 한정자를 이용해서 선언한다.
    • 추상 프로퍼티는 추상 메서드와 마찬가지로 자식 클래스에서 반드시 오버라이드 해야한다.
abstract class MyAbstractClass
{
    // 구현을 가지는 프로퍼티의 경우
    private string name;
    public string Name
    {
        get { return name; } 
    }

    // 구현이 없는 abstract 프로퍼티의 경우
    public abstract int Number { get; set; }
}

class MyClass : MyAbstractClass
{
    // abstract 프로퍼티는 반드시 재정의해야 한다. 
    public override int Number { get; set; } 
}



람다식과 프로퍼티 #

  • 프로퍼티는 본문을 간단하게 람다식으로 구현할 수 있다.
  • 이것을 식 본문 멤버(Expression-Bodied Member) 라고 한다.
class MyClass
{
    // 읽기, 쓰기 모두 가능한 프로퍼티 
    private string name;
    public string Name
    {
        get => name;
        set => name = value;
    }

    // 읽기 전용 프로퍼티
    private int age;
    public int Age => age;
}