C #에서 String이 값 유형처럼 동작하는 참조 유형 인 이유는 무엇입니까?


371

문자열은 불변이고 같은 객체를 참조하지 않고 텍스트를 비교하기 위해 == 오버로드되는 것과 같은 값 유형의 특성을 대부분 가지고 있지만 참조 유형입니다.

그렇다면 왜 문자열이 값 유형이 아닌가?


불변 유형의 경우 구별은 대부분 구현 세부 사항 ( is테스트 제외)이므로 "역사적인 이유"일 것입니다. 불변 개체를 물리적으로 복사 할 필요가 없기 때문에 복사 성능이 이유가 될 수 없습니다. 이제 실제로 is검사 (또는 유사한 제약 조건)를 사용하는 코드를 중단하지 않고 변경할 수 없습니다 .
Elazar

BTW는 C ++에 대한 동일한 대답입니다 (값과 참조 유형의 구별은 언어에서는 명시 적이 지 않지만) std::string모음처럼 행동 하기로 결정하는 것은 현재 해결할 수없는 오래된 실수입니다.
Elazar

답변:


333

문자열은 값이 크지 않으므로 힙에 저장해야하기 때문에 값 유형이 아닙니다. 값 유형은 (아직 CLR의 모든 구현에서) 스택에 저장됩니다. 스택 할당 문자열은 모든 종류의 것들을 망칠 것입니다 : 스택은 32 비트의 경우 1MB, 64 비트의 경우 4MB입니다. 각 문자열을 상자에 넣어 복사해야하며 벌금을 내야하며 인턴 문자열을 사용할 수 없으며 메모리 사용이 필요합니다 풍선 등

(편집 : 구현 세부 사항 인 값 유형 스토리지에 대한 설명이 추가되어 System.ValueType에서 상속되지 않는 값 sematics가있는 유형이 있습니다. 감사합니다. 벤)


75
나는 여기서 nitpicking하고 있지만 질문과 관련된 블로그 게시물에 링크 할 수있는 기회를 제공하기 때문에 단지 가치 유형이 반드시 스택에 저장되는 것은 아닙니다. ms.net에서는 가장 일반적으로 적용되지만 CLI 사양에서 전혀 지정하지는 않습니다. 값과 참조 유형의 주요 차이점은 참조 유형이 값별 복사 시맨틱을 따르는 것입니다. 참조 blogs.msdn.com/ericlippert/archive/2009/04/27/...blogs.msdn.com/ericlippert/archive/2009/05/04/...
벤 Schwehn

8
@ Qwertie : String가변 크기가 아닙니다. 추가하면 실제로 다른 String객체를 만들어 새 메모리를 할당합니다.
codekaizen 2016 년

5
즉, 이론적으로 문자열은 값 유형 (구조) 일 수 있지만 "값"은 문자열에 대한 참조 일뿐입니다. .NET 디자이너는 자연스럽게 중개인을 제거하기로 결정했습니다 (.NET 1.0에서는 구조체 처리가 비효율적이며 문자열을 기본 유형이 아닌 참조로 이미 정의한 Java를 따르는 것이 당연합니다. 그런 다음 값 형식을 개체로 변환하면 상자에 넣어야하며 불필요하게 비효율적입니다.
Qwertie

7
@ codekaizen Qwertie가 옳지 만 문구가 혼란 스럽다고 생각합니다. 하나의 문자열은 다른 문자열과 크기가 다를 수 있으므로 실제 값 유형과 달리 컴파일러는 문자열 값을 저장하기 위해 할당 할 공간이 얼마나되는지 미리 알 수 없었습니다. 예를 들어, Int32는 항상 4 바이트이므로 문자열 변수를 정의 할 때마다 컴파일러에서 4 바이트를 할당합니다. int변수에 변수 가있을 때 (값 유형 인 경우) 컴파일러는 얼마나 많은 메모리를 할당해야 합니까? 당시 값이 아직 할당되지 않았 음을 이해하십시오.
케빈 브록

2
죄송합니다. 지금은 수정할 수 없다는 오타가 있습니다. 예를 들어, Int32는 항상 4 바이트이므로 int변수 를 정의 할 때마다 컴파일러가 4 바이트를 할당 합니다. string변수에 변수 가있을 때 (값 유형 인 경우) 컴파일러는 얼마나 많은 메모리를 할당해야 합니까? 당시 값이 아직 할당되지 않았 음을 이해하십시오.
케빈 브록

57

값 유형 인 경우 성능 (공간 및 시간!)이 끔찍하고 메소드 등에서 전달되거나 리턴 될 때마다 값을 복사해야하므로 값 유형이 아닙니다.

세계를 제정신으로 유지하기위한 가치 의미론이 있습니다. 코드를 작성하는 것이 얼마나 어려운지 상상할 수 있습니까?

string s = "hello";
string t = "hello";
bool b = (s == t);

설정 b으로 false? 모든 응용 프로그램에서 코딩이 얼마나 어려운지를 상상해보십시오.


44
자바는 화창한 것으로 알려져 있지 않습니다.
Jason Jason

3
@ 매트 : 정확히. C #으로 전환했을 때 혼란 스러웠습니다. 팀원이 "=="를 사용하는 동안 항상 문자열을 비교하기 위해 .equals (..)를 사용했기 때문에 혼란 스러웠습니다. 나는 그들이 참조를 비교하기 위해 "=="를 남기지 않은 이유를 결코 이해하지 못했지만, 생각한다면 90 %의 시간은 아마도 문자열에 대한 참조가 아닌 내용을 비교하고 싶을 것입니다.
Juri

7
@ Juri : 실제로는 참조를 확인하는 것이 바람직하지 않다고 생각합니다. 때로는 new String("foo");다른 new String("foo")참조가 동일한 참조로 평가할 수 있기 때문에 new운영자 가 기대하는 것이 아닙니다 . (또는 참고 문헌을 비교하고 싶은 경우를 말씀해 주시겠습니까?)
Michael

1
@Michael 글쎄, 당신은 null과 비교를 잡기 위해 모든 비교에 참조 비교를 포함시켜야합니다. 참조를 문자열과 비교하는 또 다른 좋은 장소는 평등 비교보다는 비교할 때입니다. 비교할 때 두 개의 동등한 문자열은 0을 리턴해야합니다.이 경우를 점검하는 것은 어쨌든 전체 비교를 수행하는 한 시간이 걸리므로 유용한 단축키가 아닙니다. 검사 ReferenceEquals(x, y)는 빠른 테스트이며 즉시 0을 반환 할 수 있으며 null 테스트와 혼합해도 더 이상 작업이 추가되지 않습니다.
Jon Hanna

1
... 문자열을 클래스 유형이 아닌 해당 스타일의 값 유형으로 사용하면 a의 기본값 string이 null 참조가 아닌 빈 문자열 (.net 이전 시스템과 동일)로 작동 할 수 있습니다. 사실, 내 자신의 환경 설정 값 형식이하는 것입니다 String참조 형을 포함 NullableString전자가에 디폴트 값에 해당 갖는, String.Empty그리고의 기본을 갖는 후자를 null, 특별 권투와 / 언 박싱 규칙 (예 : 그 권투 default- valued NullableStringString.Empty)에 대한 참조를 생성합니다 .
supercat

26

참조 유형과 값 유형의 구별은 기본적으로 언어 디자인에서 성능 상충 관계입니다. 참조 유형은 힙에 작성되므로 구성 및 소멸 및 가비지 콜렉션에 약간의 오버 헤드가 있습니다. 반면에 값 유형은 전체 객체가 포인터가 아니라 복사되기 때문에 메소드 호출에 대한 오버 헤드가 있습니다 (데이터 크기가 포인터보다 큰 경우). 문자열은 포인터 크기보다 훨씬 클 수 있으며 일반적으로 참조 유형으로 설계되었습니다. 또한 Servy가 지적했듯이 값 유형의 크기는 컴파일 타임에 알고 있어야하며 이는 항상 문자열의 경우는 아닙니다.

가변성의 문제는 별도의 문제입니다. 참조 유형과 값 유형은 모두 변경 가능하거나 변경 불가능할 수 있습니다. 가변 값 형식의 의미가 혼동 될 수 있으므로 일반적으로 값 형식은 변경할 수 없습니다.

참조 유형은 일반적으로 변경 가능하지만 의미가있는 경우 변경 불가능하도록 설계 할 수 있습니다. 문자열은 특정 최적화가 가능하기 때문에 불변으로 정의됩니다. 예를 들어, 동일한 프로그램에서 동일한 문자열 리터럴이 여러 번 발생하는 경우 (일반적인 경우) 컴파일러는 동일한 객체를 재사용 할 수 있습니다.

그렇다면 "=="문자열을 텍스트로 비교하기 위해 오버로드되는 이유는 무엇입니까? 가장 유용한 의미론이기 때문입니다. 두 문자열이 텍스트와 같으면 최적화로 인해 동일한 객체 참조 일 수도 있고 아닐 수도 있습니다. 따라서 참조를 비교하는 것은 무의미하지만 텍스트를 비교하는 것은 거의 항상 원하는 것입니다.

보다 일반적으로 말하면, Strings는 value semantics 라고 불립니다 . 이는 C # 고유의 구현 세부 사항 인 값 형식보다 일반적인 개념입니다. 값 형식에는 값 의미가 있지만 참조 형식에는 값 의미가있을 수도 있습니다. 유형에 값 의미가있는 경우 기본 구현이 참조 유형 또는 값 유형인지 실제로 알 수 없으므로 구현 세부 사항을 고려할 수 있습니다.


값 유형과 참조 유형의 차이점은 실제로 성능에 관한 것이 아닙니다. 변수가 실제 객체를 포함하는지 또는 객체에 대한 참조를 포함하는지에 관한 것입니다. 문자열의 크기는 가변적이므로 문자열은 값 유형이 될 수 없습니다. 값 유형이 되려면 일정해야합니다. 성능은 거의 관련이 없습니다. 참조 유형도 전혀 비싸지 않습니다.
Servy

2
@Sevy : 문자열의 크기 일정합니다.
JacquesB

가변 크기의 문자 배열에 대한 참조 만 포함하기 때문입니다. 실제 "값"만있는 값 유형을 참조 유형으로 사용하는 것은 여전히 ​​모든 집중적 인 목적에 대한 참조 의미론을 가지므로 더 혼란 스러울 것입니다.
Servy

1
@Sevy : 배열의 크기는 일정합니다.
JacquesB

1
배열을 생성하면 크기는 일정하지만 전 세계의 모든 배열이 정확히 같은 크기는 아닙니다. 그게 내 요점이야 문자열이 값 유형이 되려면 .NET에서 값 유형이 설계된 방식이므로 존재하는 모든 문자열이 모두 정확히 동일한 크기 여야합니다. 실제로 값을 갖기 전에 이러한 값 유형에 대한 저장 공간을 확보 할 수 있어야 하므로 컴파일시 크기를 알아야합니다 . 이러한 string유형에는 고정 크기의 char 버퍼가 있어야하며 이는 제한적이고 매우 비효율적입니다.
Servy

16

이것은 오래된 질문에 대한 늦은 대답이지만 다른 모든 대답에는 요점이 없습니다. 2005 년 .NET 2.0까지 .NET에는 제네릭이 없었습니다.

String때문에 대신 값 형식의 참조 형식은 마이크로 소프트가 그 문자열이 제네릭이 아닌 컬렉션에 가장 효율적인 방법으로 저장 될 수 있도록하는 것이 매우 중요했다 같은, System.Collections.ArrayList.

제네릭이 아닌 컬렉션에 값 형식을 저장하려면 objectboxing이라는 형식으로 특별한 변환이 필요합니다 . CLR은 값 유형을 상자에 넣을 때 값을 a 안에 System.Object넣고 관리되는 힙에 저장합니다.

컬렉션에서 값을 읽으려면 unboxing이라고하는 역 연산이 필요합니다.

권투와 unboxing 모두 무시할 수없는 비용이 있습니다 : 권투는 추가 할당이 필요하고 unboxing은 유형 검사가 필요합니다.

일부 답변 string은 크기가 가변적이므로 값 유형으로 구현 될 수 없다고 잘못 주장 합니다. 실제로 작은 문자열 최적화 전략을 사용하여 문자열을 고정 길이 데이터 구조로 구현하는 것은 쉽습니다. 문자열은 외부 버퍼에 대한 포인터로 저장되는 큰 문자열을 제외하고는 유니 코드 문자 시퀀스로 메모리에 직접 저장됩니다. 두 표현은 동일한 고정 길이, 즉 포인터의 크기를 갖도록 설계 될 수 있습니다.

제네릭이 첫날부터 존재했다면 문자열을 값 유형으로 사용하는 것이 더 간단한 의미, 더 나은 메모리 사용 및 더 나은 캐시 위치를 가진 더 나은 솔루션 일 것입니다. List<string>함유 단지 작은 문자열은 메모리의 하나의 연속 블록 수 있었다.


이 답변에 감사드립니다! 나는 힙과 스택 할당에 대해 말하는 다른 모든 답변을 살펴 보았지만 스택은 구현 세부 사항 입니다. 결국, string단지 크기와에 대한 포인터를 포함 char어쨌든 배열을, 그래서 그것은 "큰 값 유형"하지 않을 것입니다. 그러나 이것이이 디자인 결정에 대한 단순하고 관련이있는 이유입니다. 감사!
V0ldek

8

문자열 만이 변경 불가능한 참조 유형일뿐만 아니라 멀티 캐스트 델리게이트도. 그래서 작성하는 것이 안전합니다

protected void OnMyEventHandler()
{
     delegate handler = this.MyEventHandler;
     if (null != handler)
     {
        handler(this, new EventArgs());
     }
}

문자열을 사용하고 메모리를 할당하는 가장 안전한 방법이기 때문에 문자열을 변경할 수 없다고 가정합니다. 왜 그들은 가치 유형이 아닌가? 이전 작성자는 스택 크기 등에 대해 맞습니다. 또한 프로그램에서 동일한 상수 문자열을 사용할 때 참조 유형을 문자열로 지정하면 어셈블리 크기를 절약 할 수 있습니다. 정의하면

string s1 = "my string";
//some code here
string s2 = "my string";

"my string"상수의 두 인스턴스가 모두 어셈블리에 한 번만 할당 될 수 있습니다.

일반적인 참조 유형과 같은 문자열을 관리하려면 문자열을 새 StringBuilder (string s) 안에 넣습니다. 또는 MemoryStreams를 사용하십시오.

함수에 거대한 문자열이 전달 될 것으로 예상되는 라이브러리를 작성하려면 매개 변수를 StringBuilder 또는 Stream으로 정의하십시오.


1
불변의 참조 유형의 예가 많이 있습니다. 그리고 현재 구현에서 실제로 보장되는 문자열 예제는 기술적 으로는 모듈 당 (어셈블리 당이 아님)이지만 거의 동일합니다.
Marc Gravell

5
마지막으로 다시 : StringBuilder는 큰 문자열 을 전달 하려고하면 도움이되지 않습니다 (실제로 문자열로 구현되기 때문에)-StringBuilder는 문자열을 여러 번 조작 하는 데 유용합니다 .
Marc Gravell

하 슬러가 아닌 델리게이트 핸들러를 의미 했습니까? (죄송합니다. .. 그러나 그것은 내가 아는 (일반적이지 않은)성에 매우 가깝습니다 ....)
Pure.Krome

6

또한 문자열이 구현되는 방식 (플랫폼마다 다름)과 함께 스티칭을 시작할 때. 를 사용하는 것과 같습니다 StringBuilder. 그것은 당신이 끝에 도달하면, 당신이 큰 연결 성능을 수행하는 경우 큰 연결 성능을 방해하지 않기를 바랍니다, 당신을 위해 더 많은 메모리를 할당하기 위해 버퍼를 할당합니다.

Jon Skeet이 여기서 도울 수 있을까요?


5

주로 성능 문제입니다.

문자열이 LIKE 값 유형으로 작동하면 코드를 작성할 때 도움이되지만 값 유형이되면 성능이 크게 저하됩니다.

심층적 인 모양을 원하면 .net 프레임 워크의 문자열에 대한 멋진 기사 를 살펴보십시오 .


3

매우 간단한 단어에서 크기가 명확한 값은 값 유형으로 취급 될 수 있습니다.


이것은 의견이어야합니다
ρяσѕρєя K

C #을 처음 접하는 ppl에 대해 이해하기 쉬움
LONG

2

string참조 유형을 어떻게 알 수 있습니까? 그것이 어떻게 구현되는지 중요하지 않다. C #의 문자열은 정확하게 변경할 수 없으므로이 문제에 대해 걱정할 필요가 없습니다.


System.ValueType에서 파생되지 않기 때문에 참조 유형입니다 (믿습니다) System.ValueType에 대한 MSDN의 비고 값 유형은 스택 할당되거나 구조에서 인라인으로 할당됩니다. 참조 유형은 힙 할당됩니다.
Davy8

참조 및 값 유형은 궁극적 인 기본 클래스 Object에서 파생됩니다. 값 유형이 오브젝트처럼 작동해야하는 경우, 값 유형을 참조 오브젝트처럼 보이게하는 랩퍼가 힙에 할당되고 값 유형의 값이 해당 유형으로 복사됩니다.
Davy8

랩퍼는 값 유형이 포함되어 있음을 시스템이 알 수 있도록 표시됩니다. 이 프로세스를 복싱이라고하고 역 프로세스를 언 박싱이라고합니다. 복싱 및 언 박싱을 통해 모든 유형을 객체로 취급 할 수 있습니다. (
후부

2

실제로 문자열은 값 유형과 거의 유사하지 않습니다. 우선 모든 값 유형을 변경할 수있는 것은 아니며 Int32의 값을 원하는대로 변경할 수 있으며 스택의 주소는 여전히 동일합니다.

문자열은 아주 좋은 이유 때문에 불변이며 참조 유형과는 관련이 없지만 메모리 관리와는 많은 관련이 있습니다. 문자열 크기가 변경 될 때 관리되는 힙에서 물건을 옮기는 것보다 새 객체를 만드는 것이 더 효율적입니다. 나는 당신이 가치 / 참조 유형과 불변의 객체 개념을 함께 혼합한다고 생각합니다.

"=="까지 : "=="는 연산자 오버로드이며 문자열로 작업 할 때 프레임 워크를 더 유용하게 만드는 매우 좋은 이유로 다시 구현되었습니다.


나는 가치 유형이 정의에 의해 불변이 아니라는 것을 알고 있지만, 가장 좋은 방법은 자신을 만들 때 있어야한다고 제안하는 것 같습니다. 나는 가치 유형의 특성이 아니라 특성을 말했다. 나에게 종종 가치 유형이 이러한 특성을 나타내지 만 반드시 정의에 의해서는 아니라는 것을 의미한다
Davy8

5
@WebMatrix, @ Davy8 : 기본 유형 (int, double, bool, ...)은 변경할 수 없습니다.
Jason Jason

1
@Jason, 불변의 용어는 문자열 값이 변경 될 때 문자열과 같이 내부적으로 문자열의 새 인스턴스가 생성되고 원래 객체가 변경되지 않은 채 초기화와 같이 초기화 후 변경할 수없는 객체 (참조 유형)에 주로 적용된다고 생각했습니다. 이것이 가치 유형에 어떻게 적용됩니까?
WebMatrix

8
"int n = 4; n = 9;"에서 int 변수가 "상수"라는 의미에서 "불변"인 것은 아닙니다. 값 4는 불변이며 9로 변경되지 않습니다. int 변수 "n"은 먼저 4의 값을 가지며 다른 값은 9입니다. 그러나 값 자체는 불변입니다. 솔직히, 이것은 wtf에 매우 가깝습니다.
Daniel Daranas

1
+1. 나는 그들이 단순히 그렇지 않을 때이 "문자열은 가치 유형과 같다"는 말을 듣고 있습니다.
존 한나

1

문자열이 문자 배열로 구성된 것처럼 간단하지 않습니다. 문자열을 문자형 배열로 본다 []. 따라서 참조 메모리 위치가 스택에 저장되고 힙에서 어레이의 메모리 위치 시작을 가리 키기 때문에 힙에 있습니다. 문자열 크기는 힙에 할당되기 전에 알 수 없습니다.

그렇기 때문에 문자열이 같은 크기 일지라도 문자열을 변경할 때 컴파일러는이를 알지 못하고 새로운 배열을 할당하고 배열의 위치에 문자를 할당해야하기 때문에 문자열을 변경할 수 없습니다. 언어가 메모리를 즉시 할당하지 않도록 보호하는 방법으로 문자열을 생각하는 것이 합리적입니다 (프로그래밍과 같은 C 읽기)


1
"문자열 크기는 할당되기 전에 알 수 없습니다"– 이것은 CLR에서 올바르지 않습니다.
codekaizen 2018 년

-1

또 하나의 신비한 다운 투표권을 얻을 위험이 있습니다 ... 많은 사람들이 가치 유형과 기본 유형과 관련하여 스택과 메모리를 언급한다는 사실은 마이크로 프로세서의 레지스터에 맞아야하기 때문입니다. 레지스터가 가지고있는 것보다 더 많은 비트를 필요로하는 경우 스택에 무언가를 밀거나 팝할 수 없습니다.

부동 소수점 기본 유형은 80 비트 너비의 FPU에 의해 처리됩니다.

이것은 프리미티브 유형의 정의를 난독 화하기 위해 OOP 언어가 있기 오래 전에 결정되었으며 값 유형은 OOP 언어를 위해 특별히 작성된 용어라고 가정합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.