프로그래밍을 할 때에, 자료를 문자열로 관리하는 경우가 굉장히 많이 생기는데,
이전까지는 문자열을 char형의 배열을통해 관리했었다.
char배열을 통한 문자열 관리에 있어, 구조적 한계 때문에 관리하기 어려운 부분이 생긴다.
이러한 문제점을 해결하기 위해 C#에서는 문자열을 string이라는 클래스를 통해 문자열을 관리한다.
오늘은 이 string과, 문자열을 관리하는 또 다른 클래스인 StringBuilder에 대해 알아볼 것이다.
string은 앞서 설명했듯, 문자열을 관리하는 클래스이다.
그러나, string은 다른 자료형들과 같이 자료형 처럼 선언할 수 있다. 아래와같이 말이다.
using System;
namespace Favor
{
internal class Program
{
static void Main(string[] args)
{
string str = "string";
Console.WriteLine(str);
//출력값 : string
}
}
}
또, string은 배열 기반, 그것도 char배열 기반으로 이루어져있기 때문에, 각 인덱스에 접근하여 정보를 얻어올 수 있다.
using System;
namespace Favor
{
internal class Program
{
static void Main(string[] args)
{
string str = "abcdefg";
for(int i = 0; i < 3; i++)
Console.Write(str[i]);
// 출력값 : abc
}
}
}
기타 배열과 다른 점이 있다면,
다른 배열에서는 각 인덱스에 접근하여 해당하는 값을 바꿀 수 있었는데, string은 그것이 불가능하다.
void Num()
{
stirng str = "abc";
str[1] = 'a'; //런타임 에러
}
위와 같이, 인덱스에 접근하여 특정 값을 조정하려고 하면 런타임 에러가 발생하게된다.
런타임 에러의 로그를 살펴보면, string str은 읽기 전용이라 값의 수정이 불가능하다고 알려준다.
string은 char배열기반으로 만들어져 있지만, 위와같이 인덱스의 값을 하나씩 직접 수정하지는 못하기 때문에, string 클래스 내부의 빌트인 메서드를 이용하거나, 문자열의 값 전체를 대입하여 수정해야한다.
또한, string은 아래와 같은 값의 수정도 가능하다.
using System;
namespace Favor
{
internal class Program
{
static void Main(string[] args)
{
string str = "abc";
str += 123;
Console.Write(str);
// 출력값 : abc123
}
}
}
위 처럼 더하기 연산자를 통해서 문자열에 문자를 추가로 입력할 수 있다.
이 때, 문자열을 " " 쌍따옴표 안에 넣어 추가할 수 있을 뿐만 아니라, 문자가 아닌 상수의 값을 문자열 안에 넣을 수도있다.
string은 앞서 말했듯 다른 기본 자료형과는 다르게 크기자 정해져있지 않다.
char의 집합이기 때문에, char의 갯수에 따라 그 크기가 달라진다.
실행시 크기가 결정되고, 그 크기는 서로 일정하지 않다.
string은 또한 기본 자료형이 구조체로 되어있는 것과는 다르게, class로 구현되어있기 때문에, 참조형식으로 이루어져있다.
그러나 기본 자료형처럼 값 형식으로 사용하기 위해, 값 형식 '처럼' 구현되어있다.
이를 위해, string끼리 서로 값대입이 일어난다면, 참조형식이 얕은복사를 하는 것과는 다르게 깊은 복사가 일어난다.
그러나, 이처럼 값 자체를 복사하긴 하지만, 참조형식으로, Heap영역에 해당 값이 저장되기 때문에
string이 설정되면 변경할 수 없다는 불변성을 특징으로 갖고있다.
string은 클래스이며, 그렇기에 string 클래스 내부에 내장되어있는 메서드, 빌트인 메서드를 사용하여 문자열을 보다 편리하게 관리하는 것이 가능하다.
아래는 빌트인 메서드 기능들의 예시이다.
Indexof : 현재 문자열 내에서 찾고자 하는 지정된 문자 또는 문자열의 위치를 앞에서부터 찾는 기능 (int로 index를 반환)
LastIndexof : Indexof를 역순으로 돌려서 찾음 (int로 index를 반환)
Startswith : 현재 문자열이 지정된 문자열로 시작되는지 검사 (bool로 반환)
EndsWith : 현재 문자열이 지정된 문자열로 끝나는지 검사. (bool로 반환)
contains : 현재 문자열이 지정된 문자열을 포함하는지 검사. (bool로 반환)
Replace : 현재 문자열에서, 지정된 문자열을 지정한 문자열로 모두 바꾼 문자열을 반환 old를 new로 변환
Equals : 현재 문자열과 지정한 문자열이 같은지 검사. (bool로 반환)
Trim : 앞, 뒤의 공백 제거 (중간에 삽입된 공백은 제거하지 않음)
SubString : 지정된 인덱스 부터 지정된 인덱스 만큼 문자열을 뽑아냄 (string을 반환)
Split : 분할을 해준다. 잘게잘게 (기준이 되는 문자를 입력하면, 기준점을 만날 때 마다 기준은 지우고, 배열에 저장해줌)
string은 앞서 말했듯 char배열을 기반으로 만들어진 클래스다.
이 말은 곧, string은 클래스기 때문에, 참조형식으로 되어있다는 뜻이다.
참조형식은 값 형식과는 다르게, 특정 값을 힙 영역에 저장하고, 해당 값의 주소를 갖는 변수가 스택 영역에 저장된다고 했었다.
그럼 이렇게 값을 바꾸었을 때는 어떤 일이 일어나게 될까?
using System;
namespace Favor
{
internal class Program
{
static void Main(string[] args)
{
string str = "abc";
str += 123;
str + "def";
str += 456;
Console.Write(str);
// 출력값 : abc123def456
}
}
}
위에서는 str의 값이 처음 선언된 abc에서, abc123, abc123def, abc123def456으로 총 3번 바뀌게 되었다.
이렇게 값이 바뀌었을 때는, 같은 공간에 있는 문자열의 값이 바뀌는 것이 아니다.
왜냐하면, 문자열의 길이가 늘어날 수록, 차지하는 메모리의 크기가 달라지기 때문에 다시 새로 메모리영역을 할당해야하기 때문이다.
abc가 있던 자리에는 abc123이 들어갈 수 없고, abc123이 있던 자리에는 abc123def가 들어갈 자리가 없다.
이는 위에서 설명한 string의 불변성 때문에 그렇다. 런타임시 설정된 string객체는 값이 변하지 않는다.
따라서 str은 값이 바뀔 때 마다 새로운 힙 영역에 메모리를 할당하고, 그 공간의 주소를 갖게된다.
대입되는 값에 따라 그에 맞는 메모리를 할당하는 것을 동적할당이라고 하는데, string은 동적배열이라고 할 수 있다.
동적할당을 하는 경우에는 메모리 누수를 막기위해 해당 메모리의 사용이 끝나면, 할당을 해제시켜줘야한다.
그런데 string은 값을 바꿀 때 마다 동적할당을 하긴 하는데, 이를 해제시켜주는 부분이 없다.
왜냐하면, c#에는 전에 말했던 가비지 컬렉터가 할당은 되었으나 사용되지 않는 메모리들을 수거하여 동적할당하는 역할을 수행하기 때문이다.
가비지 컬렉터를 다룰 때에 중요한 점은, 너무 자주 호출되게 하여 최적화에 유의해야 한다.
하지만 이렇게 string의 값을 자주 바꾸면, 바꾸는 만큼 가비지 컬렉터가 호출되기 때문에, 위와같이 문자열의 값을 자주 바꿔야하는 상황을 만들지 않거나, 빌트인 메서드를 통해 값을 조정하는 것이 필요하다.
그렇다면, 문자열의 값을 자주 바꿀 때에 용이한 방법은 없을까?
문자열의 변경이 잦은 경우에는 StringBuilder 클래스를 사용할 수 있다.
문자열 변경에 있어, string에 비해 굉장히 높은 효율을 보여준다.
using System;
namespace Favor
{
internal class Program
{
static void Main(string[] args)
{
const int Test = 100000;
Stopwatch strWatch = Stopwatch.StartNew();
string strRes = "";
for(int i = 0; i < Test; i++)
strRes += "a";
strWatch.Stop();
Console.WriteLine($"String : {strWatch.ElapsedMilliseconds} ms");
Stopwatch sbWatch = Stopwatch.StartNew();
StringBuilder sbRes = new StringBuilder();
for(int i = 0; i < Test; i++)
sbRes.Append("a");
sbWatch.Stop();
Console.WriteLine($"StringBuilder : {sbWatch.ElapsedMilliseconds} ms");
// 출력값 : String : 800ms
StringBuilder : 0ms
}
}
}
위의 Stopwatch는 시간을 계산하기 위해 사용한 클래스이다.
string과 StringBuilder의 연산속도를 비교해보았다.
각 Res개체에 a라는 글자를 10만번 누적시키는데 얼마나 걸리는지를 재보았을 때,
string의 값 변경 연산에는 800밀리세컨드 정도가 소모되었지만, StringBuilder의 연산에는 0ms라고 나왔다.
이 값은 연산 오류나 출력 오류가 아닌, 실제로 0ms 이하의 값이 나왔기에 0ms라고 출력된 것이다.
이 처럼 압도적인 성능차를 보여주기 때문에 값 변경에 있어서는 StringBuilder를 사용하는 것이 좋다.
그러면 이 StringBuilder는 어떨 때 사용하며, 어떻게 사용할지를 알아보자.
using System;
namespace Favor
{
internal class Program
{
static void Main(string[] args)
{
StringBuilder sb = new StringBuilder();
string str1 = "abc";
string str2 = "def";
sb.Append(str1);
sb.Append(str2);
Console.WriteLine(sb);
// 출력값 : abcdef
}
}
}
우선, 앞서 시간계산을 했을 때 처럼 StringBuilder객체를 인스턴스 해야한다.
인스턴스된 sb를 통해 StringBuilder의 빌트인 메서드를 사용하는 것이 가능하다.
빌트인 메서드 중 대표적인 기능은 문자열을 더해주는 기능인 Append()다.
이는 이름 뜻 그대로 문자열에 문자열을 추가로 넣는 것이다.
위에서 sb에 str1을 Append했을 때, sb에는 "abc"가 추가되었고,
sb에 str2를 Append했을 때, "abc"에 "def"가 추가되어 abcdef가 출력된다.
앞서, string 문자열은 값 형식처럼 사용하기 위해 얕은 복사가 아닌 깊은 복사가 일어난다고 설명했었다.
그렇기 때문에 StringBuilder를 이용한 문자열의 추가 역시 깊은복사를 통해 값으로 전달된다.
Append()의 기능을 좀 더 알아보면, Append는 StringBuilder객체에 소괄호 안으로 전달된 매개변수 문자열을 복사하여, 그 복사본을 기존에 있던 StringBuilder 객체의 뒤에 추가하는 것이다.
데이터를 관리할 때, 문자열로 관리하는 경우가 굉장히 많기 때문에, string의 구조적 특징을 잘 이해하고, StringBuilder의 기능을 잘 파악하여 문자열을 효율적으로 관리하는 것이 굉장히 중요하다.
따라서 앞으로 문자열을 자주 관리하며 문자열 관리에 익숙해질 필요가 있겠다.
'C# 일기' 카테고리의 다른 글
| 15. 연산자 오버로딩 (0) | 2024.03.08 |
|---|---|
| 14. Array 클래스 (0) | 2024.03.08 |
| 12. 객체지향의 특징 (0) | 2024.03.06 |
| 11. 인터페이스 (Interface) (2) | 2024.03.06 |
| 10. 추상 클래스(Abstract Class) (2) | 2024.03.06 |