본문 바로가기

C#/이것이 C#이다

[ 이것이 C#이다 ] Chapter 09 . 프로퍼티

[ 학습 흐름 ]

[ 학습 흐름 ]

  1. public 필드
  2. 프로퍼티
  3. 생성자
  4. 무명 형식
  5. 인터페이스의 프로퍼티
  6. 추상 클래스의 프로퍼티

[ public필드의 유혹 ]

[ public ]

class MyClass
{
    private int myField;
    public int GetMyField(){return myField;}
    public void SetMyField(int MyValue){myField = newValue;}
}

MyClass obj = new MyClass();
obj.SetMyField(3);
Console.WriteLine(obj.GetMyField());

- 필드를 public으로 선언하고 , Get/Set 메서드 대신 = 연산자로 필드를 읽거나 할당하는 것은 편하지만 은닉성을 해친다 .

- Get/Set 메서드를 통해 은닉성을 지킬수 있지만, 편의성은 떨어진다 .


[ 메소드 보다 프로퍼티 ]

[ 프로퍼티 ]

class MyClass
{
    private int myField;
    public int MyField
    {
    	get
        {
        	return myField;
        }
        set
        {
        	myField=value;
        }
    }
}

myClass obj = new MyClass();
obj.MyField=3;
Console.WriteLine(obj.MyField);

- get/set을 접근자라고 한다 .

- get 접근자는 필드로부터 값을 읽어온다

- set 접근자는 암묵적 매개변수인 value 키워드를 통해 필드에 값을 할당한다 .

- 프로퍼티를 구현하면 해당 클래스는 = 할당 연산자로 데이터를 저장하고 읽어올 수도 있다 .


[ 읽기 전용 프로퍼티 ]

class MyClass
{
	get
    {
    	return MyField;
    }
}

- get메서드만 구현하면 해당 프로퍼티는 읽기 전용 프로퍼티가 된다 .

- 쓰기 전용 프로퍼티도 문법적으로는 가능하나 , 사용할 프로그래머에게 쓰기 전용 프로퍼티의 용도,동작 결과의

확인 방법을 알려줘야 한다 . 그렇지 않다면 해당 프로퍼티는 코드를 관리하기 어렵게 만든다 .

using System;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace Property;

class BirthdayInfo
{
    private string name;
    private DateTime birthDay;
    public string Name
    {
        get { return name; }
        set { name = value; }
    }
    public DateTime BirthDay
    {
        get { return birthDay; }
        set { birthDay = value; }
    }
    public int Age
    {
        get{return new DateTime(DateTime.Now.Subtract(birthDay).Ticks).Year;}
    }
}

class MainApp
{
    static void Main(string[] args)
    {
        BirthdayInfo birth = new BirthdayInfo();
        birth.Name = "서현";
        birth.BirthDay = new DateTime(1991, 6, 28);
        Console.WriteLine($"Name : {birth.Name}");
        Console.WriteLine($"BirthDay : {birth.BirthDay.ToShortDateString()}");
        Console.WriteLine($"Age : {birth.Age}");
    }
}

[ 자동 구현 프로퍼티 ]

[ 자동 구현 프로퍼티 ]

public class NameCard
{
    //자동 구현 프로퍼티
    public string Name
    {
        get;set;
    }
    //자동 구현 프로퍼티를 선언과 동시에 초기화
    public string Name{get;set;}="UnKnown"
}

- 프로퍼티는 데이터의 오염에 관해서는 메서드처럼 안전하고,데이터를 다룰때는 필드처럼 간결하다 .

- 단순히 읽고 씀에는 자동 구현 프로퍼티를 사용한다 .

- 필드를 생성 할 필요도 없다

- 자동 구현 프로퍼티에 초기값이 필요할때 생성자에 초기화 코드 작성할 필요없이 선언과 동시에 초기화가 가능하다 .


[ 프로퍼티와 생성자 ]

[ 생성자 ]

- 객체를 생성할 때 프로퍼티를 이용해 매개변수를 입력받아 각 필드를 초기화 할 수 있다 .

- 매개변수가 있는 생성자와 다르게 모든 프로퍼티를 초기화 할 필요 없다 .

using System;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace ConstructorWithProperty;

class BirthdayInfo
{
    public string Name{get;set;}
    public DateTime BirthDay { get; set; }
    public int Age
    {
        get{return new DateTime(DateTime.Now.Subtract(BirthDay).Ticks).Year;}
    }
}

class MainApp
{
    static void Main(string[] args)
    {
        BirthdayInfo birth = new BirthdayInfo()
        {
            Name = "서현",
            BirthDay = new DateTime(1991,6,28)
        };

        Console.WriteLine($"Name : {birth.Name}");
        Console.WriteLine($"BirthDay : {birth.BirthDay.ToShortDateString()}");
        Console.WriteLine($"Age : {birth.Age}");
    }
}

[ 초기화 전용 자동 구현 프로퍼티 ]

[ 초기화 전용 자동 구현 프로퍼티 ]

class Transaction
{
	public Transaction(string from,string to,int amount)
    {
    	this.from=from,this.to=to,this.amount=amount;
    }
    
    string from;
    string to;
    int amount;
    
    public string From{get{return from;}}
    public string To{get{return to;}}
    public int Amount{get{return amount;}}
}

- 의도치 않은 데이터오염을 방지하는 장치는 접근 한정자,readonly 필드,readonly 구조체, 튜플등이 있다 .

- 생성자를 통해 필드를 초기화 , 그 필드에 접근하는 프로퍼티는 get 접근자만 가지도록 했다 . (읽기전용)

public class Transaction
{
	public string From(get;init;)
    public string To(get;init;)
    public int Amount(get;init;)
}

- init 접근자를 통해 외부에서 프로퍼티를 변경 가능하지만 , 객체를 초기화 하는 순간에만 가능하게 한다 .

- 초기화 된 이후로 변경 할 수 없고 , 필드에 get 만 가능하다 .

- 객체 초기화가 된 이후 초기화 전용 자동구현 프로퍼티를 수정하려 한다면 컴파일 에러가 발생한다 .


[ 프로퍼티 초기화를 강제하는 required ]

[ required ]

- 수식하는 프로퍼티를 객체가 초기화 될 때 반드시 초기화 되도록 컴파일 수준에서 강제한다 .

using System;


namespace RequiredProperty;

class BirthdayInfo
{
    public required string Name{get;set;}
    public required DateTime BirthDay { get; init; }
    public int Age
    {
        get{return new DateTime(DateTime.Now.Subtract(BirthDay).Ticks).Year;}
    }
}

class MainApp
{
    static void Main(string[] args)
    {
        BirthdayInfo birth = new BirthdayInfo()
        {
        	//required 한정했기에 초기화가 없다면 컴파일 에러
            Name = "서현",
            BirthDay = new DateTime(1991,6,28)
        };

        Console.WriteLine($"Name : {birth.Name}");
        Console.WriteLine($"BirthDay : {birth.BirthDay.ToShortDateString()}");
        Console.WriteLine($"Age : {birth.Age}");
    }
}

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

[ 불변 객체 ]

불변객체의 정의

- 내부 상태 (데이터)를 변경할 수 없는 객체

- 위 특성때문에 데이터 복사와 비교가 많이 이뤄진다 .

- 불변객체는 참조 형식은 모든 필드를 readonly로 / 값 형식은 readonly struct로 구조체를 선언한다 .


값 형식의 복사

- 값 형식 객체는 다른 객체 할당시 깊은 복사 수행 .

- 필드가 많을수록 , 여러곳에서 사용해야 할수록 복사비용은 커짐


참조 형식의 복사

- 참조 형식은 메모리 주소만 복사하기에 이런 오버헤드가 없음

-  그러나 직접 깊은 복사의 구현이 필요함


값 형식의 비교

- 모든 필드를 1:1 비교


참조 형식의 비교

- 비교시 직접 비교 코드 작성

- 보통 object로 부터 상속하는 Equals()메소드를 오버라이딩


참조 형식의 불변객체의 장점

- 함수 호출 인수, 컬렉션 요소로 사용시 복사비용을 줄일 수 있다


값 형식의 불변객체의 장점

- 새 상태 표현과 상태 확인을 위해 깊은 복사와 내용비교가 필수이기에 편리하다


[ 레코드 ]

레코드 형식

- 레코드는 불변 객체에서 복사와 비교를 편리하게 수행하기 위해 도입된 형식

- 값 형식처럼 다룰 수 있는 불변 참조 형식 . 참조 형식의 비용 효율과 값 형식이 주는 편리함 제공


레코드 선언하기

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

//레코드로 인스턴스를 만들면 불변 객체가 생성
RTransaction tr1 = new RTransactioin{From="Alice",To="Bob",Amount=100};

- 레코드는 record 키워드와 초기화 전용 자동 구현 프로퍼티를 함께 이용하여 선언

- 쓰기 가능한 프로퍼티,필드도 자유롭게 선언해 넣을 수 있다 .

using System;

namespace Record;

record RTransaction
{
    public string From { init; get; }
    public string To { init; get; }
    public int Amount { init; get; }
    public override string ToString()
    {
        return $"{From,-10}->{To,-10}:${Amount}";
    }
}
class MainApp
{
    static void Main(string[] args)
    {
        RTransaction tr1=new RTransaction { From="Aclice",To="Bob",Amount=100};
        RTransaction tr2 = new RTransaction { From = "Aclice", To = "Charlie", Amount = 100 };

        Console.WriteLine(tr1);
        Console.WriteLine(tr2);

    }
}

 


[ with를 이용한 레코드 복사 ]

- C# 컴파일러는 레코드 형식을 위한 복사 생성자를 자동으로 작성한다 

- 이는 protected 로 선언되기 때문에 명시적 호출은 어렵고 , with 식을 이용해야 한다 .

- with 식은 tr1을 복사 , To 프로퍼티 값만 수정해서 tr라는 새로운 레코드 객체를 생성한다 .

- with 식은 객체 상태(프로퍼티)가 다양할수록 유용하다 .

using System;

namespace Record;

record RTransaction
{
    public string From { init; get; }
    public string To { init; get; }
    public int Amount { init; get; }
    public override string ToString()
    {
        return $"{From,-10}->{To,-10}:${Amount}";
    }
}
class MainApp
{
    static void Main(string[] args)
    {
        RTransaction tr1=new RTransaction { From="Aclice",To="Bob",Amount=100};
        RTransaction tr2 = tr1 with { To = "Charlie" };
        RTransaction tr3 = tr2 with { From = "Dave", Amount = 30 };

        Console.WriteLine(tr1);
        Console.WriteLine(tr2);
        Console.WriteLine(tr3);
    }
}

 


[ 레코드 객체 비교하기 ]

- C# 컴파일러는 레코드의 상태를 비교하는 Equals()메소드를 자동으로 구현한다 .

- 클래스 객체는 Equals 메서드 오버라이딩을 통해 직접 구현해야 한다 .

- 레코드는 참조 형식이지만 값 형식처럼 Equals()메소드를 구현하지 않아도 비교가 가능하다 .

using System;

namespace RecordComp;

record RTransaction
{
    public string From { init; get; }
    public string To { init; get; }
    public int Amount { init; get; }
    public override string ToString()
    {
        return $"{From,-10}->{To,-10}:${Amount}";
    }
}
class CTransaction
{
    public string From { init; get; }
    public string To { init; get; }
    public int Amount { init; get; }
    public override string ToString()
    {
        return $"{From,-10}->{To,-10}:${Amount}";
    }
}
class MainApp
{
    static void Main(string[] args)
    {
        CTransaction trA = new CTransaction { From = "Alice", To = "Bob", Amount = 100 };
        CTransaction trB = new CTransaction { From = "Alice", To = "Bob", Amount = 100 };
        Console.WriteLine(trA);
        Console.WriteLine(trB);
        //Equals()의 기본구현은 내용비교가 아닌 참조비교이기에 false
        Console.WriteLine($"trA equals to trB : {trA.Equals(trB)}");

        RTransaction tr1 =new RTransaction { From="Aclice",To="Bob",Amount=100};
        RTransaction tr2 = new RTransaction { From = "Aclice", To = "Bob", Amount = 100 };


        Console.WriteLine(tr1);
        Console.WriteLine(tr2);
        Console.WriteLine($"tr1 equals to tr2 : {tr1.Equals(tr2)}");
    }
}

 


[ 무명 형식 ]

[ 무명 형식 ]

- 형식의 선언과 동시에 인스턴스를 할당한다 .

- 인스턴스를 만들고 다시는 사용하지 않을때 요긴하다 .

- 무명 형식의 프로퍼티에 할당된 값을 변경이 불가하다 .

using System;

namespace AnonymousType;

class MainApp
{
    static void Main(string[] args)
    {
        var a = new { Name = "박상현", Age = 123 };
        Console.WriteLine($"Name{a.Name} , Age{a.Age}");

        var b = new { Subject = "수학", Scores = new int[] { 90, 80, 70, 60 } };
        Console.WriteLine($"Subject: {b.Subject} Scores:");
        foreach (var score in b.Scores)
            Console.Write($"{score} ");

        Console.WriteLine();
    }
}

 


[ 인터페이스의 프로퍼티 ]

[ 인터페이스의 프로퍼티 ]

- 메소드뿐 아니라 프로퍼티 , 인덱서도 가질 수 있다 .

- 인터페이스에 들어가는 프로퍼티는 구현을 가지지 않고 , 클래스의 자동구현프로퍼티와 모습이 동일하다

- 파생 클래스는 프로퍼티 구현시 자동 구현 프로퍼티를 이용 할 수도 있다.

using System;

namespace AnonymousType;

class MainApp
{
    interface INameValue
    {
        string Name
        {
            get;
            set;
        }
        string Value
        {
            get;
            set;
        }
    }
    class NameValue:INameValue
    {
        public string Name { get; set; }
        public string Value { get ; set ; }
    }
    static void Main(string[] args)
    {
        NameValue name = new NameValue() { Name = "이름", Value = "박상현" };
        Console.WriteLine("{name.Name} : {name.Value}");
    }
}

[ 추상 클래스의 프로퍼티 ]

[ 추상 클래스의 프로퍼티 ]

abstract class 추상 클래스 이름
{
	abstract 데이터형식 프로퍼티이름
    {
    	get;set;
    }
}

- 클래스처럼 구현된 프로퍼티를 가질 수 있는 한편, 인터페이스처럼 구현되지 않은 프로퍼티도 가질 수 있다 .

- 이것을 추상 프로퍼티라고 한다 .

using System;

namespace PropertiesInAbstractClass;

abstract class Product
{
    static int serial = 0;
    //구현을 가진 프로퍼티
    public string SerialID
    {
        get { return String.Format("{0:d5}", serial++); }
    }
    //추상 프로퍼티
    abstract public DateTime ProductDate { get; set; }
}
class MyProduct:Product
{
    public override DateTime ProductDate { get; set; }
}


class MainApp
{
    static void Main(string[] args)
    {
       Product product1 = new MyProduct() { ProductDate=new DateTime(2023,1,10)};
       Console.WriteLine("Product: {0}, ProductDate : {1}",product1.SerialID,product1.ProductDate);
        Product product2 = new MyProduct() { ProductDate = new DateTime(2023, 4, 10) };
        Console.WriteLine("Product: {0}, ProductDate : {1}", product2.SerialID, product2.ProductDate);
    }
}