언제 클래스 대신 구조체를 사용해야합니까?


302

MSDN은 가벼운 객체가 필요할 때 구조체를 사용해야한다고 말합니다. 클래스보다 구조체가 선호되는 다른 시나리오가 있습니까?

어떤 사람들은 그것을 잊었을 수도 있습니다.

  1. 구조체 는 메소드를 가질 수 있습니다.
  2. 구조체 는 상속 될 수 없습니다.

구조체와 클래스의 기술적 차이점을 이해하지만 구조체 를 사용할 기분이 좋지 않습니다 .


이 맥락에서 대부분의 사람들이 잊어 버리는 경향은 C #에서 구조체도 메소드를 가질 수 있다는 것입니다.
petr k.

답변:


295

MSDN에는 다음과 같은 대답이 있습니다. 클래스와 구조 간 선택 .

기본적으로이 페이지는 4 가지 항목 점검 목록을 제공하며 유형이 모든 기준을 충족하지 않는 한 클래스를 사용하도록 지시합니다.

유형에 다음 특성이 모두없는 한 구조를 정의하지 마십시오.

  • 기본 유형 (정수, 이중 등)과 유사한 단일 값을 논리적으로 나타냅니다.
  • 인스턴스 크기가 16 바이트보다 작습니다.
  • 불변입니다.
  • 자주 박스에 넣을 필요는 없습니다.

1
어쩌면 나는 명백한 것을 놓치고 있지만 "불변의"부분에 대한 추론을 얻지 못합니다. 왜 이것이 필요한가요? 누군가 설명해 주시겠습니까?
Tamas Czinege

3
구조체가 불변 인 경우 참조 의미가 아닌 값 의미가 있는지는 중요하지 않기 때문에 아마도 이것을 추천했을 것입니다. 사본을 만든 후 객체 / 구조를 변경 한 경우에만 차이가 중요합니다.
Stephen C. Steel

@ DrJokepu : 어떤 상황에서, 시스템은 구조체의 임시 복사본을 만든 다음이를 변경하는 코드를 참조하여 해당 복사본을 전달할 수 있습니다. 임시 사본이 제본되므로 변경 사항이 유실됩니다. 구조체에 돌연변이 메서드가있는 경우이 문제가 특히 심각합니다. c #과 vb.net의 일부 결함에도 불구하고, 가변 구조체는 다른 방법으로는 달성 할 수없는 유용한 의미론을 제공하기 때문에 가변성은 클래스를 만드는 이유라는 견해에 강력하게 동의하지 않습니다. 클래스에 대한 불변의 구조체를 선호하는 의미 론적 이유는 없습니다.
supercat

4
@Chuu : JIT 컴파일러를 설계 할 때 Microsoft는 16 바이트 이하의 구조체를 복사하기위한 코드를 최적화하기로 결정했습니다. 이것은 17 바이트 구조체를 복사하는 것이 16 바이트 구조체를 복사하는 것보다 상당히 느릴 수 있음을 의미합니다. Microsoft가 이러한 최적화를 더 큰 구조체로 확장 할 것으로 예상하는 특별한 이유는 없지만 17 바이트 구조체는 16 바이트 구조체보다 복사 속도가 느릴 수 있지만 큰 구조체가 더 효율적인 경우가 많다는 점에 유의해야합니다. 큰 클래스 객체와 구조체 의 크기에 따라 구조체 의 상대적인 이점이 커지는 위치 .
supercat

3
@Chuu : 클래스와 같은 큰 구조에 동일한 사용 패턴을 적용하면 코드가 비효율적 일 수 있지만, 구조체를 클래스로 대체하지 않고 대신 구조체를보다 효율적으로 사용하는 적절한 솔루션입니다. 가장 주목할만한 것은 값으로 구조체를 전달하거나 반환하지 않아야합니다. 로 전달 ref그렇게하는 것이 합리적 때마다 매개 변수를 설정합니다. 값이 4 개인 필드를 가진 구조체를 수정 된 버전을 반환하는 메서드에 전달하는 것보다 4,000 개의 필드를 가진 구조체를 ref 매개 변수로 ref 매개 변수로 전달하는 방법을 변경하십시오.
supercat

53

이전 답변을 읽지 않은 것에 놀랐습니다. 가장 중요한 측면을 고려합니다.

ID가없는 유형을 원할 때 구조체를 사용합니다. 예를 들어 3D 점 :

public struct ThreeDimensionalPoint
{
    public readonly int X, Y, Z;
    public ThreeDimensionalPoint(int x, int y, int z)
    {
        this.X = x;
        this.Y = y;
        this.Z = z;
    }

    public override string ToString()
    {
        return "(X=" + this.X + ", Y=" + this.Y + ", Z=" + this.Z + ")";
    }

    public override int GetHashCode()
    {
        return (this.X + 2) ^ (this.Y + 2) ^ (this.Z + 2);
    }

    public override bool Equals(object obj)
    {
        if (!(obj is ThreeDimensionalPoint))
            return false;
        ThreeDimensionalPoint other = (ThreeDimensionalPoint)obj;
        return this == other;
    }

    public static bool operator ==(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
    {
        return p1.X == p2.X && p1.Y == p2.Y && p1.Z == p2.Z;
    }

    public static bool operator !=(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
    {
        return !(p1 == p2);
    }
}

이 구조체의 인스턴스가 두 개인 경우 메모리의 단일 데이터인지 두 개인 지 상관하지 않습니다. 당신은 그들이 보유하고있는 가치에 관심이 있습니다.


4
주제를 벗어 버렸지 만 obj가 ThreeDimensionalPoint가 아닌 경우 ArgumentException을 발생시키는 이유는 무엇입니까? 이 경우 거짓을 반환해서는 안됩니까?
Svish

4
맞습니다, 나는 열망했습니다. return false거기에 있었어야했던 것이 지금 바로 수정되었습니다.
Andrei Rînea

구조체를 사용하는 흥미로운 이유. 여기에 표시된 것과 비슷한 GetHashCode 및 Equals 클래스를 정의했지만 사전 키로 사용하는 경우 해당 인스턴스를 변경하지 않도록 항상주의해야했습니다. 내가 구조체로 정의했다면 아마 더 안전했을 것입니다. (따라서 키는 구조체가 사전 키가 된 순간 필드 의 사본 이 되므로 나중에 원본을 변경해도 키는 변경되지 않습니다.)
ToolmakerSteve

귀하의 예에서는 12 바이트 만 있지만 16 바이트를 초과하는 구조체에 많은 필드가있는 경우 클래스를 사용하고 GetHashCode 및 Equals 메소드를 재정의하는 것을 고려해야한다는 것을 명심하십시오.
다니엘 보테 코레아

DDD의 값 형식이 반드시 C #의 값 형식을 사용해야한다는 의미는 아닙니다.
Darragh

26

Bill Wagner는 그의 책 "effective c #"( http://www.amazon.com/Effective-Specific-Ways-Improve-Your/dp/0321245660 ) 에서 이에 관한 장을 가지고 있습니다. 그는 다음 원리를 사용하여 결론을 맺습니다.

  1. 유형 데이터 스토리지의 주요 책임은 무엇입니까?
  2. 공용 인터페이스는 데이터 멤버에 액세스하거나 수정하는 속성으로 만 정의됩니까?
  3. 타입에 서브 클래스가 절대 없을까요?
  4. 유형이 다형성으로 처리되지 않습니까?

네 가지 질문 모두에 '예'라고 대답하면 구조체를 사용하십시오. 그렇지 않으면 클래스를 사용하십시오.


1
그래서 ... 데이터 전송 객체 (DTO)는 구조체 여야합니까?
크루저

1
위에서 설명한 4 가지 기준을 충족하면 예라고 대답합니다. 왜 데이터 전송 객체를 특정한 방식으로 처리해야합니까?
Bart Gijssens

2
@cruizer는 상황에 따라 다릅니다. 한 프로젝트에서 우리는 DTO에 공통 감사 필드를 가지고 있었으므로 다른 사람들로부터 상속받은 기본 DTO를 작성했습니다.
Andrew Grothe

1
(2)를 제외한 모든 것은 훌륭한 원칙처럼 보입니다. (2)가 정확히 무엇을 의미하는지, 왜 그런지 아는 그의 추론을보아야 할 것입니다.
ToolmakerSteve

1
@ToolmakerSteve :이 책을 읽어야합니다. 책의 많은 부분을 복사 / 붙여 넣기하는 것이 공정하다고 생각하지 마십시오.
Bart Gijssens


12

다음과 같은 경우 구조체를 사용합니다.

  1. 객체는 읽기 전용이어야합니다 (복사 될 구조체를 전달 / 할당 할 때마다). 읽기 전용 객체는 대부분의 경우 잠금이 필요하지 않으므로 멀티 스레드 처리에 유용합니다.

  2. 물체는 작고 수명이 짧습니다. 이러한 경우 관리 힙에 배치하는 것보다 훨씬 효율적인 스택에 오브젝트가 할당 될 가능성이 높습니다. 객체가 할당 한 메모리는 더 이상 범위를 벗어나면 해제됩니다. 즉, 가비지 콜렉터의 작업이 적고 메모리가 더 효율적으로 사용됩니다.


10

다음과 같은 경우 수업을 이용하십시오 :

  • 정체성이 중요합니다. 값으로 메소드에 전달 될 때 구조가 암시 적으로 복사됩니다.
  • 메모리 공간이 큽니다.
  • 해당 필드에는 이니셜 라이저가 필요합니다.
  • 기본 클래스에서 상속해야합니다.
  • 다형성 동작이 필요합니다.

다음과 같은 경우 구조를 사용하십시오.

  • 기본 유형 (int, long, byte 등)처럼 작동합니다.
  • 메모리 사용량이 적어야합니다.
  • 값으로 구조를 전달해야하는 P / Invoke 메소드를 호출하고 있습니다.
  • 가비지 수집이 응용 프로그램 성능에 미치는 영향을 줄여야합니다.
  • 해당 필드는 기본값으로 만 초기화해야합니다. 이 값은 숫자 유형의 경우 0, 부울 유형의 경우 false, 참조 유형의 경우 null입니다.
    • C # 6.0에서 구조체에는 구조체의 필드를 기본값이 아닌 값으로 초기화하는 데 사용할 수있는 기본 생성자가있을 수 있습니다.
  • 모든 구조체가 상속하는 ValueType 이외의 기본 클래스에서 상속 할 필요는 없습니다.
  • 다형성 동작이 필요하지 않습니다.

5

메소드 호출에서 물건을 다시 전달하기 위해 몇 가지 값을 그룹화하고 싶을 때 항상 구조체를 사용했지만 그 값을 읽은 후에는 아무것도 사용할 필요가 없습니다. 물건을 깨끗하게 유지하는 방법으로. 나는 구조체의 사물을 "쓰레기"라고 생각하고 수업의 사물을 더 유용하고 "기능적"으로 보는 경향이있다


4

엔터티를 변경할 수없는 경우 구조체 또는 클래스를 사용할지 여부에 대한 문제는 일반적으로 의미가 아니라 성능 중 하나입니다. 32/64 비트 시스템에서 클래스 참조는 클래스의 정보량에 관계없이 4/8 바이트를 저장해야합니다. 클래스 참조를 복사하려면 4/8 바이트를 복사해야합니다. 다른 한편으로, 모든 별개의클래스 인스턴스는 보유하고있는 정보와 이에 대한 참조의 메모리 비용 외에 8/16 바이트의 오버 헤드를 갖습니다. 각각 4 개의 32 비트 정수를 보유하는 500 개의 엔티티 배열을 원한다고 가정하십시오. 엔티티가 구조 유형 인 경우 배열은 500 개의 엔티티가 모두 동일하거나 모두 다르거 나 사이에 관계없이 8,000 바이트를 필요로합니다. 엔티티가 클래스 유형 인 경우 500 개의 참조 배열에는 4,000 바이트가 필요합니다. 이러한 참조가 모두 다른 객체를 가리키는 경우 객체는 각각 추가 24 바이트 (500 개당 12,000 바이트), 총 16,000 바이트 (구조 유형의 스토리지 비용의 두 배)가 필요합니다. 반면에 코드가 하나의 객체 인스턴스를 생성 한 다음 500 개의 모든 배열 슬롯에 대한 참조를 복사 한 경우 총 비용은 해당 인스턴스의 24 바이트, 4, 배열의 경우 000-총 4,024 바이트입니다. 주요 절감 효과. 마지막 상황뿐만 아니라 거의 상황이 해결되지 않지만 경우에 따라 공유를 가치있게 만들기 위해 충분한 배열 슬롯에 일부 참조를 복사 할 수 있습니다.

엔티티가 변경 가능 해야하는 경우 클래스 또는 구조체를 사용할지 여부에 대한 질문이 더 쉽습니다. "Thing"은 x라는 정수 필드를 가진 구조체 또는 클래스이고 다음 코드를 수행한다고 가정합니다.

  일 t1, t2;
  ...
  t2 = t1;
  t2.x = 5;

후자의 진술이 t1.x에 영향을 미치 길 원합니까?

Thing이 클래스 유형 인 경우 t1과 t2는 동일하므로 t1.x와 t2.x도 동일합니다. 따라서 두 번째 문장은 t1.x에 영향을 미칩니다. Thing이 구조 유형 인 경우 t1과 t2는 서로 다른 인스턴스가되므로 t1.x와 t2.x는 서로 다른 정수를 나타냅니다. 따라서 두 번째 문장은 t1.x에 영향을 미치지 않습니다.

.net에는 구조체 돌연변이 처리에 약간의 단점이 있지만 가변 구조와 가변 클래스는 근본적으로 다른 동작을합니다. 값 유형 동작을 원할 경우 ( "t2 = t1"은 t1과 t2를 별개의 인스턴스로 남겨두고 t1에서 t2로 데이터를 복사 함을 의미 함) .net에서 값 유형을 처리 할 때 문제가 생길 수있는 경우 구조. 밸류 타입 시맨틱을 원하지만 .net의 쿼크로 인해 애플리케이션에서 밸류 타입 시맨틱이 깨질 경우 클래스를 사용하고 중얼 거린다.


3

또한 위의 우수한 답변 :

구조는 가치 유형입니다.

Nothing 으로 설정할 수 없습니다 .

구조체 = Nothing을 설정하면 모든 값 유형이 기본값으로 설정됩니다.


2

실제로 행동이 필요하지 않지만 간단한 배열이나 사전보다 더 많은 구조가 필요합니다.

후속 조치 이것은 일반적으로 구조체를 생각하는 방식입니다. 나는 그들이 방법을 가질 수 있다는 것을 알고 있지만, 전반적인 정신 구분을 유지하는 것을 좋아합니다.


왜 그런 말을 해? 구조체에는 메소드가있을 수 있습니다.
Esteban Araya

2

@Simon이 말했듯이 구조체는 "값 형식"의미를 제공하므로 내장 데이터 형식과 유사한 동작이 필요한 경우 구조체를 사용하십시오. 구조체는 복사본으로 전달되므로 크기가 약 16 바이트인지 확인하려고합니다.


2

오래된 주제이지만 간단한 벤치 마크 테스트를 제공하고자했습니다.

두 개의 .cs 파일을 만들었습니다.

public class TestClass
{
    public long ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public struct TestStruct
{
    public long ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

벤치 마크 실행 :

  • 1 개의 TestClass 생성
  • TestStruct 1 개 생성
  • 100 개의 TestClass 생성
  • 100 TestStruct 만들기
  • 10000 TestClass 생성
  • 10000 TestStruct 만들기

결과 :

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
Intel Core i5-8250U CPU 1.60GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.101
[Host]     : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT  [AttachedDebugger]
DefaultJob : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT


|         Method |           Mean |         Error |        StdDev |     Ratio | RatioSD | Rank |    Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------- |---------------:|--------------:|--------------:|----------:|--------:|-----:|---------:|------:|------:|----------:|

|      UseStruct |      0.0000 ns |     0.0000 ns |     0.0000 ns |     0.000 |    0.00 |    1 |        - |     - |     - |         - |
|       UseClass |      8.1425 ns |     0.1873 ns |     0.1839 ns |     1.000 |    0.00 |    2 |   0.0127 |     - |     - |      40 B |
|   Use100Struct |     36.9359 ns |     0.4026 ns |     0.3569 ns |     4.548 |    0.12 |    3 |        - |     - |     - |         - |
|    Use100Class |    759.3495 ns |    14.8029 ns |    17.0471 ns |    93.144 |    3.24 |    4 |   1.2751 |     - |     - |    4000 B |
| Use10000Struct |  3,002.1976 ns |    25.4853 ns |    22.5920 ns |   369.664 |    8.91 |    5 |        - |     - |     - |         - |
|  Use10000Class | 76,529.2751 ns | 1,570.9425 ns | 2,667.5795 ns | 9,440.182 |  346.76 |    6 | 127.4414 |     - |     - |  400000 B |

1

흠 ...

구조체 대 클래스 사용에 대한 가비지 수집을 인수로 사용하지 않습니다. 관리되는 힙은 스택과 매우 유사하게 작동합니다. 개체를 만들면 힙의 맨 위에 놓이므로 스택에 할당하는 것만 큼 빠릅니다. 또한 개체의 수명이 짧고 GC주기에서 살아남지 못하면 GC가 여전히 액세스 가능한 메모리에서만 작동하므로 할당 해제가 무료입니다. (MSDN을 검색하십시오. .NET 메모리 관리에 대한 일련의 기사가 있습니다.

내가 구조체를 사용하는 대부분의 경우, 나중에 참조 의미론을 갖는 것이 일을 조금 더 단순하게 만들었 음을 알게 되었기 때문에 그렇게하기 위해 스스로를 차 버린다.

어쨌든 위에 게시 된 MSDN 기사의 4 가지 사항은 좋은 지침으로 보입니다.


1
때로는 구조체와 함께 참조 의미론이 필요한 경우 간단히 선언 class MutableHolder<T> { public T Value; MutableHolder(T value) {Value = value;} }하면 a MutableHolder<T>는 클래스 의미가 변경 가능한 객체입니다 ( T구조체 또는 불변 클래스 유형 인 경우에도 마찬가지 입니다).
supercat

1

Structs는 힙이 아닌 스택에 있으므로 스레드로부터 안전하므로 전송 객체 패턴을 구현할 때 사용해야합니다. 힙에서 객체를 휘발성으로 사용하지 않으려는 경우,이 경우 콜 스택을 사용하려고합니다. 이것은 내가 대답을 끝내는 것에 놀란 구조체를 사용하는 기본 사례입니다.


-3

가장 좋은 대답은 필요한 속성 모음, 클래스가 속성 및 동작 모음 인 경우 클래스를 사용하는 것입니다.


구조체도 방법을 가질 수 있습니다
니씬 CHANDRAN에게

물론 메소드가 필요한 경우 99 %의 확률은 클래스 대신 struct를 부적절하게 사용하는 것입니다. 구조체에 메소드가있는 것이 좋을 때 발견 한 유일한 예외는 콜백입니다.
Lucian Gabriel Popescu
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.