.NET에서 문자열을 변경할 수 없다는 것을 감안할 때 왜 string.Substring()
O ( substring.Length
) 대신 O ( ) 시간 이 걸리 도록 설계되었는지 궁금합니다 O(1)
.
즉, 트레이드 오프는 무엇입니까?
.NET에서 문자열을 변경할 수 없다는 것을 감안할 때 왜 string.Substring()
O ( substring.Length
) 대신 O ( ) 시간 이 걸리 도록 설계되었는지 궁금합니다 O(1)
.
즉, 트레이드 오프는 무엇입니까?
답변:
업데이트 : 나는이 질문을 너무 좋아, 방금 블로그에 올렸습니다. 문자열, 불변성 및 지속성을 참조하십시오
짧은 대답은 다음과 같습니다. n이 커지지 않으면 O (n)은 O (1)입니다. 대부분의 사람들은 작은 문자열에서 작은 하위 문자열을 추출하므로 복잡성이 무증상으로 성장하는 방식은 전혀 관련이 없습니다 .
긴 대답은 다음과 같습니다.
인스턴스에 대한 조작이 적은 양 (일반적으로 O (1) 또는 O (lg n))의 복사 또는 새 할당으로 원본의 메모리를 재사용 할 수 있도록 구축 된 불변 데이터 구조를 "지속적"이라고합니다. 불변 데이터 구조. .NET의 문자열은 변경할 수 없습니다. 귀하의 질문은 본질적으로 "왜 지속되지 않는가?"
일반적으로 .NET 프로그램의 문자열에서 수행 되는 작업을 살펴보면 완전히 새로운 문자열을 만드는 것이 전혀 나쁘지 않습니다 . 복잡한 영구 데이터 구조를 구축하는 데 드는 비용과 어려움은 그 자체로 비용을 지불하지 않습니다.
사람들은 일반적으로 "서브 스트링"을 사용하여 약간 긴 문자열에서 짧은 문자열 (예 : 10 ~ 20 자)을 추출합니다. 쉼표로 구분 된 파일에 텍스트 줄이 있고 성인 세 번째 필드를 추출하려고합니다. 줄은 몇 백 자 정도 될 것이고 이름은 수십 개가 될 것입니다. 현대 하드웨어 에서는 50 바이트의 문자열 할당 및 메모리 복사가 매우 빠릅니다 . 기존 문자열의 가운데에 길이를 더한 포인터로 구성된 새로운 데이터 구조를 만드는 것도 놀랍도록 빠릅니다. "충분히 빠름"은 정의상 충분히 빠릅니다.
추출 된 부분 문자열은 일반적으로 크기가 작고 수명이 짧습니다. 가비지 콜렉터가 곧 회수 할 예정이며, 처음에는 힙에 많은 공간을 차지하지 않았습니다. 따라서 대부분의 메모리 재사용을 장려하는 지속적인 전략을 사용하는 것도 승리가 아닙니다. 내부 포인터 처리에 대해 걱정해야하기 때문에 가비지 수집기가 느려집니다.
사람들이 일반적으로 문자열에서 수행하는 하위 문자열 작업이 완전히 다른 경우 지속적인 접근 방식을 사용하는 것이 좋습니다. 사람들이 일반적으로 백만 자의 문자열을 가지고 있으며 수 천 자 범위의 크기를 가진 수천 개의 겹치는 부분 문자열을 추출하고 그 부분 문자열이 힙에서 오랫동안 살았다면 지속적인 부분 문자열을 사용하는 것이 완벽합니다. 접근하다; 그것은 낭비적이고 어리석은 짓입니다. 그러나 대부분의 업무용 프로그래머는 이런 종류의 것들과 같은 모호한 행동을하지 않습니다.. .NET은 Human Genome Project의 요구에 맞춘 플랫폼이 아닙니다. DNA 분석 프로그래머는 이러한 문자열 사용 특성과 관련된 문제를 매일 해결해야합니다. 당신이하지 않을 확률이 좋습니다. 밀접하게 일치하는 자신의 영구적 인 데이터 구조를 구축 않는 극소수의 사람들 자신의 사용 시나리오를.
예를 들어, 우리 팀은 입력 할 때 C # 및 VB 코드를 즉시 분석하는 프로그램을 작성합니다. 이러한 코드 파일 중 일부는 엄청나 므로 하위 문자열을 추출하거나 문자를 삽입 또는 삭제하기 위해 O (n) 문자열 조작을 수행 할 수 없습니다. 우리는 텍스트 버퍼에 대한 편집 내용을 나타 내기 위해 영구적 인 불변 데이터 구조를 많이 만들었습니다.이를 통해 기존 문자열 데이터의 대량 및 기존 편집시의 어휘 및 구문 분석 을 빠르고 효율적으로 재사용 할 수 있습니다. 이것은 해결하기 어려운 문제였으며 솔루션은 C # 및 VB 코드 편집의 특정 도메인에 맞게 조정되었습니다. 내장 문자열 유형이 우리 에게이 문제를 해결할 것으로 기대하는 것은 비현실적입니다.
string contents = File.ReadAllText(filename); foreach (string line in content.Split("\n")) ...
또는 다른 버전. 전체 파일을 읽은 다음 다양한 부분을 처리해야합니다. 이러한 종류의 코드는 문자열이 지속적 일 경우 상당히 빠르며 적은 메모리를 필요로합니다. 각 줄을 복사하는 대신 항상 파일 사본을 메모리에 정확히 하나만 저장 한 다음 각 줄의 일부를 처리합니다. 그러나 Eric이 말한 것처럼 일반적인 사용 사례는 아닙니다.
String
는 지속적인 데이터 구조로 구현됩니다 (표준에는 지정되어 있지 않지만 제가 알고있는 모든 구현은이 작업을 수행합니다).
정확하게 때문에 문자열은 불변, .Substring
원래 문자열의 적어도 일부의 사본을해야합니다. n 바이트 의 복사본을 만드는 데 O (n) 시간이 걸립니다.
일정한 시간 에 많은 바이트를 복사한다고 생각 하십니까?
편집 : Mehrdad는 문자열을 전혀 복사하지 말고 문자열을 참조하도록 제안합니다.
.Net에서는 누군가가 .SubString(n, n+3)
(문자열의 중간에있는 n에 대해) 호출하는 멀티 메가 바이트 문자열을 고려하십시오 .
이제 하나의 참조가 4자를 보유하고 있기 때문에 ENTIRE 문자열을 가비지 수집 할 수 없습니까? 그것은 말도 안되는 공간 낭비처럼 보입니다.
또한 하위 문자열에 대한 참조를 추적하고 (하위 문자열에있을 수도 있음) GC를 피하기 위해 최적의 시간에 복사하려고하면 개념이 악몽이됩니다. 에 복사 .SubString
하고 간단한 불변 모델을 유지하는 것이 훨씬 간단하고 안정적 입니다.
편집 : 다음 은 큰 문자열 내에서 하위 문자열에 대한 참조를 유지하는 위험에 대해 잘 읽었습니다 .
memcpy
으로는 여전히 O (n) 인 것을 사용합니다.
char*
부분 문자열을 얻을 수 있습니다 .
NULL
. Lippert의 post에 설명 된대로 처음 4 바이트에는 문자열 길이가 포함됩니다. 이것이 Skeet이 지적한 것처럼 \0
문자 를 포함 할 수있는 이유 입니다.
Java (.NET과 반대)는 두 가지 방법을 제공 Substring()
하므로 참조 만 유지할 것인지 전체 하위 문자열을 새 메모리 위치에 복사 할 것인지 고려할 수 있습니다.
단순 .substring(...)
은 내부적으로 사용 된 char
배열을 원본 String 객체와 공유 한 다음 new String(...)
필요한 경우 새 배열에 복사 할 수 있습니다 (원래의 가비지 수집을 방해하지 않도록).
이런 종류의 유연성은 개발자에게 가장 적합한 옵션이라고 생각합니다.
.substring(...)
.
Java는 더 큰 문자열을 참조하는 데 사용되었지만 다음과 같습니다.
그래도 향상 될 수 있다고 생각합니다. 복사를 조건부로 복사하는 것이 어떻습니까?
부분 문자열이 부모 크기의 절반 이상이면 부모를 참조 할 수 있습니다. 그렇지 않으면 사본을 만들 수 있습니다. 이렇게하면 많은 메모리 누수가 발생하지 않으면서도 상당한 이점이 있습니다.
char[]
(시작 및 끝에 대한 다른 포인터 사용)에서 새로운 생성으로 변경되었다는 것 String
입니다. 이것은 비용-편익 분석이 새로운 창조를위한 선호를 보여 주어야한다는 것을 분명히 보여줍니다 String
.
여기에서 "괄호 문제"를 해결 한 답변은 없습니다. 즉, .NET의 문자열은 BStr (포인터 이전에 메모리에 저장된 길이)과 CStr (문자열이 '\ 0').
따라서 "Hello there"라는 문자열은
0B 00 00 00 48 00 65 00 6C 00 6F 00 20 00 74 00 68 00 65 00 72 00 65 00 00 00
(-문 char*
에서 a에 fixed
지정된 경우 포인터는 0x48을 가리 킵니다.)
이 구조를 사용하면 문자열 길이를 빠르게 조회 할 수 있으며 (많은 컨텍스트에서 유용) 널 종료 문자열을 예상하는 Win32 (또는 기타) API에 대한 P / Invoke로 포인터를 전달할 수 있습니다.
당신이 할 때 Substring(0, 5)
당신이 복사본을 만들 필요가 말한다 규칙 "오,하지만 난 마지막 문자 뒤에 널 문자가있을 것 약속". 하위 문자열이 끝에 있더라도 다른 변수를 손상시키지 않고 길이를 넣을 곳이 없습니다.
그러나 때때로 "스트링 중간"에 대해 이야기하고 싶을 때 P / Invoke 동작에 신경 쓰지 않아도됩니다. 최근에 추가 된 ReadOnlySpan<T>
구조를 사용하여 복사없는 부분 문자열을 얻을 수 있습니다.
string s = "Hello there";
ReadOnlySpan<char> hello = s.AsSpan(0, 5);
ReadOnlySpan<char> ell = hello.Slice(1, 3);
ReadOnlySpan<char>
길이 독립적으로 저장 "부분 문자열", 그리고 그것을 값의 끝에 후 '\ 0'이 있다고 보장하지 않습니다. "문자열과 같은"여러 가지 방법으로 사용할 수 있지만 BStr 또는 CStr 특성이 없기 때문에 "문자열"이 아닙니다 (둘 다 훨씬 적음). P / Invoke를 직접 (직접) 사용하지 않으면 호출하려는 API에 ReadOnlySpan<char>
과부하 가없는 한 큰 차이가 없습니다 .
ReadOnlySpan<char>
참조 유형의 필드로서 사용될 수 없으므로, 또한 거기 ReadOnlyMemory<char>
( s.AsMemory(0, 5)
를 갖는 간접적 방식 인) ReadOnlySpan<char>
때문에 동일한 차이-단으로, string
존재한다.
이전 답변에 대한 답변 / 의견 중 일부는 가비지 수집기가 5 백만 자의 문자열을 유지하면서 5 자에 대해 계속 이야기하는 것이 낭비 적이라고 말했습니다. 이것이 바로 ReadOnlySpan<char>
접근 방식으로 얻을 수있는 행동 입니다. 짧은 계산을 수행하는 경우 ReadOnlySpan 접근 방식이 더 좋습니다. 잠시 동안 유지해야하고 원래 문자열의 작은 비율 만 유지하려는 경우 적절한 하위 문자열 (과잉 데이터를 제거하기 위해)을 수행하는 것이 좋습니다. 어딘가에 전환점이 있지만 특정 용도에 따라 다릅니다.