[객체지향 프로그래밍] 7장-9 구조체와 튜플

0

이것이 C#이다

목록 보기
16/26

7.17 구조체

C#의 복합데이터 형식에는 클래스 말고도 구조체가 있습니다. 구조체는 struct키워드를 이용해서 선언합니다. 구조체와 클래스는 필드와 메소드를 가질수있어서 상당 부분이 비슷합니다.
클래스는 실세계의 객체를 추상화하려는 데 그 존재의의가 있지만,
구조체는 데이터를 담기 위한 자료구조로 사용됩니다.
따라서 굳이 은닉성을 비롯한 객체 지향의 원칙을 구조체에 강하게 적용하지 않는 편이며, 편의를 위해 필드를 public으로 선언하는 경우가 많습니다.

이 표에 보이는 대로, 클래스는 참조 형식이고, 구조체는 값형식입니다.
따라서 구조체의 인스턴스는 스택에 할당되고 인스턴스가 선언된 블록이 끝나는 지점의 메모리에서 사라집니다.
인스턴스의 사용이 끝나면 즉시 메모리에서 제거된다는 점과,가비지 컬렉터를 덜 귀찮게 한다는 점에서 구조체는 클래스에 비해 성능의 이점을 가집니다.

구조체 예제

struct Point3D
{
	public int X;
    public int Y;
    public int Z;
    
    public Point3D(int x,int y,int z)
    {
    	this.X = x;
        this.Y = y;
        this.Z = z;
    }
    
    //Ststem.Object형식의 ToString()메소드를 오버라이딩
    public override string ToString() 	
    {
    	return string.Format($"{X}, {Y}, {Z}")
    }
}

class MainApp
{
	static void Main(string[] args)
    {
    	Point3D p3d1;							//선언만으로도 인스턴스가 생성됩니다.
        p3d1.X = 10;
        p3d1.Y = 20;
        p3d1.Z = 40;
        
        Console.WriteLine(p3d1.ToString());
        
        Point3D p3d2 = new Point3D(100,200,300);//생성자를 이용한 인스턴스 생성도 가능합니다.
        Point3D p3d3 = p3d2;					//구조체는 깊은복사가 가능합니다.
        p3d3.Z = 400;
        
        Console.WriteLine(p3d2.ToString());
        Console.WriteLine(p3d3.ToString());
    }
}

💻출력
10, 20, 40
100, 200, 300
100, 200, 400

7.17.1 구조체를 readonly로 설정해보기

객체는 속성과 기능으로 이루어진다고 설명했는데, 다른 말로 속성은 상태,기능은 행위입니다.
그러니까 객체의 속성은 필드와 프로퍼티를 통해 표현되므로, 객체의 상태 또한 필드와 프로퍼티를 통해 표현된다고 할 수 있습니다.
그리고 지금까지 우리가 다뤄왔던 것과 같이 상태의 변화를 허용하는 객체를 변경 가능 객체라고 하며, 상태 변화를 허용하지 않는 객체를 변경 불가능 객체라고 합니다.
변경 불가능 객체의 효용은 여러가지가 있지만,멀티 쓰레드간에 동기화를 할 필요가 없기 때문에 프로그램 성능 향상이 가능하고,무엇보다 버그로 인한 상태(데이터)의 오염을 막을 수 있습니다.
프로그램의 버그를 유발시키는 상태 오염의 원인을 찾기 위해 수많은 스레드를 디버깅하지 않아도 된다는 의미입니다.

구조체는 모든 필드와 프로퍼티의 값을 수정할 수 없는, 즉 변경불가능 구조체로 선언할 수 있습니다.(클래스는 변경 불가능으로 선언할 수 없습니다.) 다음과 같이 구조체를 선언할때 readonly 키워드만 기입하면 됩니다.

readonly struct ImmutableStruct
{
	public readonly int immutableField;	//OK
    public int MutableFiled;			//readonly가 없어서 컴파일 에러!
    
    public ImmutableStruct(int initValue)
    {
    	immutableField = initValue;		//생성자에서만 초기화 가능
    }
}
class SomeClass
{
	public void SomeMethod()
    {
    	ImmutableStruct is = new Immutable(123);
        is.immutableFiled = 456;		//변경불가능 객체이므로 컴파일 에러!
    }
}

readonly를 이용해서 구조체를 선언하면, 컴파일러는 해당 구조체의 모든 필드가 readonly로 선언되도록 강제합니다. 당연히 읽기 전용으로 선언된 필드를 수정하려는 시도에도 컴파일 에러가 발생합니다.

7.17.2 구조체의 메소드만 readonly로 설정해보기(읽기 전용 메소드)

읽기 전용 메소드는 구조체에서만 선언 할 수 있습니다.
예를 들어, 에어컨에서 현재온도를 화씨 온도로 변환하여 출력하는 메소드를 구현할때는
단지 출력만 하기 때문에 현재온도나 희망온도를 변경할 일이 없습니다. 따라서 잠재적 오류를 막기위해서 해당 메소드만 읽기전용으로 만들어 상태를 수정하지 않도록 만들 수 있습니다.

struct ACSetting	//(Air Conditioner = AC)
{
	public double currentInCelsius;	//현재 온도('C)
    public double target;			//희망 온도
    
    //읽기 전용메소드로 설정
    public readonly double GetFahrenheit()
    {
    	//target = currentInCelsius * 1.8 + 32;	//희망온도를 건드리려하면 컴파일 에러
        
        int fahrenheit = currentInCelsius * 1.8 + 32;	//화씨 계산
        return fahrenheit;
    }
}
class MainApp
{
	static void Main(string[] args)
    {
    	ACSetting acs;
        acs.currentInCelsius = 25;
        acs.target = 25;
        
        Console.WriteLine($"{acs.GetFahrenheit()}");
        Console.WriteLine($"{acs.target}");
    }
}

7.18 튜플

튜플도 여러필드를 담을 수 있는 구조체입니다.
하지만 평범한 구조체와 달리 튜플은 형식 이름이 없습니다. 그래서 튜플은 프로그램 전체에서 사용할 형식을 선언할 때가 아닌, 즉석에서 사용할 복합 데이터 형식을 선언할 때 적합합니다.
튜플은 구조체이므로 값형식입니다.(생성지역 벗어나면 스택에서 삭제됨)

7.18.1 명명되지 않은 튜플

//컴파일러가 튜플의 모양을 보고 직접 형식을 결정하도록 var를 이용합니다.
//튜플은 괄호 사이에 두 개 이상의 필드를 지정함으로써 만들어 집니다.
var tuple = (123,789); 
Console.WriteLine($"{tuple.Item1}, {tuple.Item2}")	//출력 : 123, 789

위 예제 같이 필드의 이름을 지정하지 않은 튜플을 일컬어 "명명되지 않은 튜플"이라고 부릅니다.
이 경우, C# 컴파일러는 자동으로 123을 Item1이라는 필드에 담고,789를 Item2라는 필드에 담습니다.

7.18.2 명명된 튜플

튜플을 조금더 예쁘게 선언하는 방법이 있습니다. 필드의 이름을 지정한 명명된 튜플이 바로 그것입니다.

var tuple = (Name:"배건하", Age: 19);
Console.WriteLine($"{tuple.Name}, {tuple.Age}")		//출력 결과 : 배건하, 19

7.18.3 튜플 분해

튜플을 분해할 수도 있습니다. 튜플을 정의 할때와 반대 모습입니다.

var tuple = (Name: "배건하",Age : 19);
var (name,age) = tuple;					//튜플 분해
Console.WriteLIne($"{name},{age}");		//출력 결과 : 배건하, 19

튜플을 분해할 때 필드를 무시하고 싶다면 _를 이용하면 됩니다.

var tuple = (Name: "배건하",Age : 19);
var (name,_) = tuple;					//age 필드는 무시
Console.WriteLIne($"{name}");			//출력 결과 : 배건하

튜플 분해를 이용하면 여러 변수를 단번에 생성하고 초기화 할 수 있습니다.
즉석에서 튜플을 만들어서 분해하는거죠.

var (name2, age2) = ("박문수", 34);	
Console.WriteLine($"{name2},{age2}");	//출력 결과 : 박문수, 34

7.18.4 명명된 튜플 = 명명되지 않은 튜플

명명되지 않은 튜플과 명명된 튜플끼리는 필드의 수와 형식이 같으면 할당 가능합니다.

var unnamed = ("스파이더맨", 9999); 					// (string, int)
var named = (Name:"박상현", age: 18);				// (string, int)

named = unamed;						    			// 명명된 튜플 = 명명되지 않은 튜플
Console.WriteLine($"{named.Name},{named.age}");	   	// 출력 결과 : 스파이더맨, 9999 

named = ("데드풀",10000);

unnamed = named;									//명명되지 않은 튜플 = 명명된 튜플
Console.WriteLine($"{unnamed.Name},{unnamed.age}");	//출력 결과 : 데드풀, 10000

7.18.5 패턴매칭

튜플이 분해가 가능한 이유는 분해자를 구현하고 있기 때문인데요. 분해자를 구현하고 있는 객체를 분해한 결과를 switch문에 분기 조건으로 활용 할 수 있습니다. 이것을 패턴매칭이라고 합니다. 식별자나 데이터 형식이 아닌 분해된 요소의 위치에 따라 값이 일치하는지를 판단하는 것이죠. 다음은 위치 패턴 매칭을 switch식이나 switch문으로 구현한 것입니다.

class MainApp
{
	//switch 식으로 구현
    private static double GetDiscountRate_Switch_Expression(object client)
    {
    	ValueTuple<string, int> result = (ValueTuple<string, int>)client;
        return result switch
        {
            ("학생", int n) when n < 18 => 0.2,
            ("학생", _) => 0.1,
            ("일반", int n) when n < 18 => 0.1,
            ("일반", _) => 0.05,
            _ => 0,
        };
    }
    
	//switch 문으로 구현
	private static double GetDiscountRate(ValueTuple<string,int> client)
    {
        double descountRate = 0.0;
        
        switch (client)
        {
            //분해방법1 : 학생 & 18세 미만
            case ValueTuple<string, int> t when t.Item1 == "학생" && t.Item2 < 18:   
                descountRate = 0.2;
                break;
            //분해방법2 : 학생 & 18세 이상
            case ("학생",_):                        
                descountRate = 0.1;
                break;
            //분해방법3 : 일반 & 18세 미만
            case var(a, b) when a == "일반" && b < 18:                              
                descountRate = 0.1;
                break;
            //분해방법4 : 일반 & 18세 이상
            case ("일반",_):
                descountRate = 0.05;
                break;
        }

        return descountRate;
    }
    
	static void Main(string[] args)
	{
		var alice 		= (jop: "학생", age: 17);
	    var bob 		= (jop: "학생", age: 23);
	    var charline 	= (jop: "일반", age: 15);
	    var dave 		= (jop: "일반", age: 21);
	    
	    Console.WriteLine($"alice		: {GetDiscountRate(alice)}");
	    Console.WriteLine($"bob			: {GetDiscountRate(bob)}");
	    Console.WriteLine($"charline	: {GetDiscountRate(charline)}");
	    Console.WriteLine($"dave		: {GetDiscountRate(dave)}");
	}
}

0개의 댓글