'이것이 C#이다 개정판'의 chapter 7을 정리한 글입니다.
구조체
구조체는 추상화와 은닉성이 강하게 적용되는 클래스와 다르게, 데이터를 담기 위한 자료구조로 주로 사용됩니다.
그렇기 때문에 은닉성을 비롯한 객체지향의 원칙을 구조체에 강하게 적용하지 않는 편이며,
편의를 위해 필드를 public으로 선언해서 사용하는 경우가 많습니다.
struct 구조체_이름
{
// 필드 ...
// 메소드 ...
}
[클래스 vs 구조제]
| 특징 | 클래스 | 구조체 |
| 키워드 | class | struct |
| 형식 | 참조 형식 (힙에 할당) | 값 형식 (스택에 할당) |
| 복사 | 얕은 복사 (Shallow Copy) | 깊은 복사 (Deep Copy) |
| 인스턴스 생성 | new 연산자와 생성자 필요 | 선언만으로도 생성 |
| 생성자 | 매개변수 없는 생성자 선언 가능 | 매개변수 없는 생성자 선언 불가능 |
| 상속 | 가능 | 값 형식이므로 상속 불가능 |
구조체는 값 형식이기 때문에 할당 연산자 '='을 통해 모든 필드가 그대로 복사됩니다.
[예시]
MyStruct s;
s.MyField1 = 1;
s.MyField2 = 2;
MyStruct t;
t = s;
s.MyField1 = 3;
s의 MyField1은 3, MyField2는 2이지만, t의 MyField1은 1, MyField2는 2입니다.
[구조체 예시]
using System;
namespace Structure
{
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;
}
public override string ToString() // System.Object 형식의 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());
}
}
}

변경불가능 객체 (Immutable Object)
객체에는 상태(State, 속성)의 변화를 허용하는 변경가능 객체(Mutable Object)와 상태의 변화를 허용하지 않는 변경불가능 객체가 존재합니다.
변경불가능 객체의 장점
- 스레드 안전성(Thread Safety) 보장
- 객체 상태가 변하지 않기 때문에 멀티 쓰레드 간에 동기화를 할 필요가 없습니다.
그로 인해 프로그램 성능 향상이 가능하고, 무엇보다 버그로 인한 상태(데이터)의 오염을 막을 수 있습니다.
- 객체 상태가 변하지 않기 때문에 멀티 쓰레드 간에 동기화를 할 필요가 없습니다.
- 예측 가능성과 안전성 증가
- 객체가 생성된 이후 상태가 변하지 않으므로, 코드의 동작이 더 예측 가능하고 디버깅이 쉬워집니다.
- 외부에서 객체 상태를 변경할 수 없으므로, 의도하지 않은 상태 변경으로 인한 버그를 줄일 수 있어 안전성이 증가됩니다.
- 캐싱 및 재사용 용이
- 불변 객체(변경불가능 객체)는 생성된 이후 상태가 절대 변하지 않기 때문에, 동일한 상태의 객체를 여러 번 새로 만들 필요 없이 한 번 만든 객체를 여러 곳에서 안전하게 공유할 수 있습니다.
- 즉, 같은 값을 가진 객체를 한 번 생성해서 저장(캐싱)해두고 필요할 때마다 재사용할 수 있습니다.
C# 주요 타입 분류
| 타입 종류 | 대표 예시 | 값 타입 / 참조형 타입 | 가변 객체 / 불변 객체 |
| 기본 값 타입 | int, float, double, bool, char byte, long, short, decimal |
값 타입 | 불변 |
| 구조체 | DateTime, TimeSpan, Vector3(유니티) 사용자 정의 struct |
값 타입 | 설계에 따라 다름 |
| 열거형 | 사용자 정의 열거형 | 값 타입 | 불변 |
| 클래스 | 사용자 정의 클래스, string, List<t> | 참조형 타입 | 가변 단, string은 불변 |
| 인터페이스 | IDisposable, IEumerable<T> | 구현체에 따라 다름 | 구현체에 따라 다름 |
| 배열 | int[ ], string[ ] | 참조형 타입 | 가변 (요소 변경 가능) |
| 델리게이트 | 대리자 함수 타입 | 참조형 타입 | 불변 |
| 튜플 | Tuble<T1, T2>, (int, string) | 둘 다 존재 | 불변 |
변경불가능 구조체
구조체는 모든 필드와 프로퍼티의 값을 수정할 수 없는, 즉 변경불가능 구조체로 선언할 수 있습니다.
※ 이에 반해 클래스는 변경불가능으로 선언할 수 없습니다.
readonly struct 구조체_이름
{
// ...
}
readonly를 이용해서 구조체를 선언하면, 컴파일러는 해당 구조체의 모든 필드가 readonly로 선언되도록 강제합니다.
readonly로 선언된 구조체 안에서 readonly로 선언되지 않은 필드와 프로퍼티는 컴파일 에러를 일으킵니다.
readonly struct ImmutableStruct
{
public readonly int ImmutableField;
public int MutableField; // 컴파일 에러발생
}
읽기 전용으로 선언된 필드를 수정하려는 시도에 대해서도 컴파일 에러가 발생합니다.
readonly struct ImmutableStruct
{
public readonly int ImmutableField;
public ImmutableStruct(int initValue)
{
immutableField = initValue; // 생성자에서만 초기화 가능
}
}
// ...
class SomeClass
{
public void SomeMethod()
{
ImmutableStruct is = new Immutable(123);
is.immutableField = 456; // 컴파일 에러 발생
}
}
읽기 전용 메소드
읽기 전용 메소드는 메소드에게 상태를 바꾸지 않도록 강제합니다.
읽기 전용 메소드 역시 구조체에서만 선언이 가능합니다. 이때 구조체는 불변가능한 구조체가 아니여도 상관 없습니다.
public readonly double GetFahrenheit()
{
target = currentInCelsius * 1.8 + 32; // 화씨 계산 결과를 target에 저장
return target; // target 반환
}
readonly로 한정한 메소드에서 객체의 필드를 바꾸려 들면 컴파일 에러가 발생합니다.
튜플(Tuple)
튜플은 여러 필드를 담을 수 있는 구조체입니다.
하지만 앞서 살펴봤던 구조체와는 달리 튜플은 형식 이름이 없습니다.
튜플은 응용 프로그램 전체에서 사용할 형식을 선언할 때가 아닌, 즉석에서 사용할 복합 데이터 형식을 선언할 때 적합합니다.
튜플은 구조체이므로 값 형식입니다.
[튜플 선언]
var tuble = (123, 789);
튜플의 선언은 위의 코드와 같습니다. 위의 코드처럼 필드의 이름을 지정하지 않는 튜플을 일컬어 '명명되지 않은 튜플'이라 합니다.
컴파일러가 튜플의 모양을 보고 직접 형식을 결정하도록 var을 이용해 선언하며, 괄호 사이에 두 개 이상의 필드를 지정함으로써 만들어집니다.
'명명된 튜플'은 아래와 같이 선언합니다.
var tuble = (Name: "홍길동", Age : 20);
Console.WriteLine($"{tuple.Name}, {tuple.Age}"); // 출력 결과 : 홍길동, 20
[튜플 분해]
튜플 분해를 이용하면 여러 변수를 단번에 생성하고 초기화할 수 있습니다.
튜플을 분해하는 이유는 튜플에 담긴 여러 값을 개별 변수로 쉽고 직관적으로 꺼내서 사용하기 위해서입니다.
var tuple = (Name : "홍길동", Age : 20);
var (name, age) = tuple; // 튜플 분해
Console.WriteLine($"{name}, {age}"); // 출력 결과 : 홍길동, 20
// 특정 필드 무시하여 튜플 분해
var tuple = (Name : "홍길동", Age : 20);
var (name, _) = tuple; // 튜플 분해
Console.WriteLine($"{name}"); // 출력 결과 : 홍길동
// 여러 변수를 생성하고 초기화
var (name, age) = ("홍길동", 20);
Console.WriteLine($"{name}, {age}"); // 출력 결과 : 홍길동, 20
튜플이 분해가 가능한 이유는 분해자(Deconstructor)를 구현하고 있기 때문입니다.
분해자를 구현하고 있는 객체를 분해한 결과를 switch 문이나 switch 식의 분기 조건에 활용할 수 있습니다.
이를 '패턴 매칭(Positional Pattern Matching)'이라고 합니다.
식별자나 데이터 형식이 아닌 분해된 요소의 위치에 따라 값이 일치하는지를 판단하는 것입니다.
[패턴 매칭 예시]
var alice = (job : "학생", age : 17);
var bob = (job : "학생", age : 23);
var charlie = (job : "일반", age : 15);
var dave = (job : "일반", age : 21);
var discountRate = alice switch
{
("학생", int n) when n < 18 => 0,2, // 학생 & 18세 미만
("학생", _) => 0.1, // 학생 & 18세 이상
("일반", int n) when n < 18 => 0.1, // 일반 & 18세 미만
("일반", _) => 0.05, // 일반 & 18세 이상
_=> 0,
};
// alice : 0.2
// bob : 0.1
// charlie : 0.1
// dave : 0.05
[예시]
using System;
namespace Tuple
{
class MainApp
{
static void Main(string[] args)
{
// 명명되지 않은 튜플
var a = ("철수", 10);
Console.WriteLine($"{a.Item1}, {a.Item2}");
// 명명된 튜플
var b = (Name : "영수", Age: 20);
Console.WriteLine($"{b.Name}, {b.Age}");
// 분해 1
var (name, age) = b;
Console.WriteLine($"{name} {age}");
// 분해 2
var (name2, age2) = ("민수", 30);
Console.WriteLine($"{name2}, {age2}");
// 분해 3
(var name3, var age3) = a;
Console.WriteLine($"{name3} {age3}");
// 명명된 튜플 = 명명되지 않은 튜플
b = a;
Console.WriteLine($"{b.Name}, {b.Age}");
}
}
}

[연습 문제]
- 다음 코드에서 오류를 찾고 오류의 원인을 설명하세요.
class A { }
class B : A { }
class C
{
public static void Main()
{
A a = new A();
B b = new B();
A c = new B();
B d = new A(); // 컴파일 오류 발생
}
}
B d = new A(); 부분에서 컴파일 오류가 발생합니다.
클래스 B는 A를 상속받은 자식 클래스이고, A는 B의 부모 클래스입니다.
부모 클래스 객체를 자식 클래스 변수에 직접 할당할 수 없습니다.
즉, A 타입의 객체(new A())는 B 타입으로 자동 형변환(업캐스팅)할 수 없습니다.
반대로, B 타입 객체는 A 타입 변수에 할당할 수 있습니다.
'C#' 카테고리의 다른 글
| [C#] 프로퍼티, 레코드 (0) | 2025.04.25 |
|---|---|
| [C#] 인터페이스 (0) | 2025.04.24 |
| [C#] 객체지향 프로그래밍 : 은닉성, 상속성, 다형성 (0) | 2025.04.20 |
| [C#] 객체지향 프로그래밍 : 객체, 클래스 (0) | 2025.04.15 |
| [C#] 메소드 (0) | 2025.04.11 |