[C# 11 과 .NET 7] 8. 공용 .NET Type
.NET에는 일반적인 개발과정에서 사용할 수 있는 숫자, 문자열, collection등과 span, index, range와의 작업, network access 등 몇 가지 공용 type들을 포함하고 있습니다.
1. 숫자 다루기
Data에 관한 가장 일반적인 작업중 하나가 바로 숫자입니다. 아래표는 .NET에서 숫자에 관한 가장 일반적인 type을 나타내고 있습니다.
Namespace | Example Type | Description |
System | SByte, Int16, Int32, Int64 | 정수로서 음수, 양수, 0 |
System | Byte, UInt16, UInt32, UInt64 | 기수로서 0, 양수 / 부호가 없으므로 U로 표현 |
System | Half, Single, Double | 실수로서, 부동소수점 수 |
System | Decimal | 실수(accurate real)로서 과학, 공학, 금융분야에서 사용 |
System.Numbers | BigInteger, Complex, Quaternion | 임의의 큰 정수, 복소수, 4차원 |
.NET은 .NET Framework 1.0부터 32bit float과 64bit double type을 지원해 왔습니다. IEEE 754 명세는 또한 16bit floating-point 표준을 정의하였는데 Machine learning의 일부 algorithm에서 해당 type을 필요로 하므로 Microsoft는 System.Half type을 .NET 5부터 포함하였습니다.
현재 C#언어는 half에 대한 별칭을 따로 정의하지 않았으므로 System.Half를 그대로 사용해야 합니다.
(1) Big integer 사용
C# 별칭이 있는 .NET type에서 저장가능한 가장 큰 정수는 18조 5천억으로 unsigned ulong integer입니다. 만약 이것보다 더 큰 수를 저장해야 한다면 BigInteger를 사용해야 합니다.
Visual Studio에서 csStudy08 solution을 만들고 여기에 WorkingWithNumbers이름의 Console App project를 추가합니다. 그런 뒤 Program.cs에서 기존구문을 모두 삭제하고 ulong type의 최댓값과 BigInteger를 통해 30 자릿수의 표시하는 문을 아래와 같이 추가합니다.
using System.Numerics;
ulong big = ulong.MaxValue;
Console.WriteLine($"{big,40:N0}");
BigInteger bigger = BigInteger.Parse("123456789012345678901234567890");
Console.WriteLine($"{bigger,40:N0}");
예제에서 사용한 format code 40은 오른쪽으로 40자리수 만큼 정렬하고자 하는 것이므로 두 숫자 모두 오른쪽 가장자리로 줄지어 표시하게 되며 N0은 천 단위 분리자와 소수점 0자리 사용을 의미합니다.
예제를 실행하면 다음과 같은 결과를 표시할 것입니다.
(2) 복소수 사용
복소수는 a + bi로 표현될 수 있으며 이때 a와 b는 실수, i는 허수 단위이고 여기서 i² = -1입니다. 실수부 a가 0이면 순허수, 허수부 b가 0이면 이것은 실수입니다.
복소수는 많은 STEM(science, technology, engineering and mathematics)연구분야에서 실질적으로 응용되고 있으며 가수의 실수와 허수 부분이 별도로 더해짐으로써 추가되었습니다.
(a + bi) + (c + di) = (a + c) + (b + d)i |
WorkingWithNumbers project의 Program.cs에서 아래와 같이 2개의 복소수를 더하는 문을 추가합니다.
Complex c1 = new(real: 4, imaginary: 2);
Complex c2 = new(real: 3, imaginary: 7);
Complex c3 = c1 + c2;
Console.WriteLine($"{c1} added to {c2} is {c3}");
Console.WriteLine("{0} + {1}i added to {2} + {3}i is {4} + {5}i", c1.Real, c1.Imaginary, c2.Real, c2.Imaginary, c3.Real, c3.Imaginary);
예제를 실행하면 다음과 같은 결과를 표시할 것입니다.
(3) Quaternion(4원수)
Quaternions은 복소수를 확장자는 숫자체계로서 실수를 넘어 4차원 연관 규범 분할 대수를 형성하며 따라서 domain도 형성합니다. 사실 실제 활용해야 할 상황이 아니라면 이런 내용들에 대한 자세한 이해할 필요는 없는데, 이들은 공간을 묘사하는 데 사용되며 따라서 다수의 computer simulation과 비행 제어 system, video game engine에서 많이 사용되고 있습니다.
(4) 난수 생성
난수가 필요하지 않다면 Random class의 instance는 아래와 같이 생성할 수 있습니다.
Random r = new();
Random class는 난수 생성의 초기화에 사용되는 seed 값을 지정하는 매개변수를 가지고 있습니다.
Random r = new(Seed: 46378);
아래글에서 이미 언급하였지만
[.NET/C#] - [C# 11 과 .NET 7] 2. C#
매개변수의 이름은 camel case로 사용하길 권장하고 있습니다. 하지만 예제에서 보듯 해당 class의 생성자를 정의한 개발자가 해당 규칙을 어기는 바람에 seed가 아닌 Seed로 사용되고 있습니다. 다시 말하지만 Seed가 아닌 seed가 되어야 합니다.
Memory를 더 할당하지 않기 위해 .NET 6에서는 Random의 공유 정적 instance를 포함하고 있습니다.
Random r = Random.Shared;
일단 Random 개체를 생성하게 되면 임의의 숫자를 생성하는 method를 아래와 같이 호출할 수 있습니다.
int ranInt = r.Next(minValue: 1, maxValue: 7); // 1에서 6사이값
double randomReal = r.NextDouble(); // 0.0에서 1.0보다 작은값
byte[] arrayOfBytes = new byte[256];
r.NextBytes(arrayOfBytes); //256 random byte array
Next method는 minValue와 maxValue 2개의 매개변수를 필요로 합니다. 다만 이때 maxValue값은 method가 반환하는 최댓값을 의미하는 것이 아님에 주의해야 합니다. 이는 'exclusive upper bound(배타적 상한선)'라고 하는 것으로 반환하는 최댓값보다 1 더 많아야 함을 의미합니다. 이와 비슷한 경우로 NextDouble method에서 반환되는 값은 0.0보다 많거나 같고 1.0보다 작습니다. NextBytes는 주어진 array만큼 random 한 byte(0에서 255) 값으로 채워줍니다.
암호화와 같이 진정한 random값이 필요한 경우를 위해 RandomNumberGenerator와 같은 특별한 type을 사용할 수 있습니다.
2. Text
Text는 변수를 통해 사용되는 가장 일반적인 data type이라고 할 수 있습니다. 아래 표는 text와 관련된 .NET의 일반적인 type을 나타내고 있습니다.
Namespace | Type | Description |
System | Char | 단일 한문자 |
System | String | text |
System.Text | StringBuilder | 고성능 문자열 다루기 |
System.Text.RegularExpressions | Regex | 고성능 문자열 pattern-matching |
(1) 문자열 길이 확인하기
Text를 다루게 되는 경우 사용되는 몇 가지 일반적인 상황들이 있습니다. 예를 들어 string변수에 저장된 문자열의 길이를 알아내는 경우 등이 그것입니다.
csStudy08 solution에 WorkingWithText console app project를 추가하고 Program.cs에서 기본의 문을 모두 삭제한 뒤 도시 이름을 저장하기 위한 변수를 정의하고 console을 통해 도시이름과 길이를 출력하는 문을 아래와 같이 작성합니다.
string city = "Seoul";
Console.WriteLine($"{city} is {city.Length} characters long.");
예제를 실행하면 다음과 같은 결과를 표시할 것입니다.
(2) 문자열에서 문자 확인하기
String class는 문자열을 저장하기 위해 내부적으로 문자에 대한 array를 사용하며 또한 한문자를 읽기 위해 array 구문을 사용할 수 있는 indexer를 가지고 있습니다. Array indexer는 0부터 시작하므로 세 번째 문자라면 index는 2가 됩니다.
Program.cs에서 string변수의 첫 번째 문자와 네 번째 문자를 표시하는 문을 아래와 같이 작성합니다.
Console.WriteLine($"First char is {city[0]} and fourth is {city[3]}.");
예제를 실행하면 다음과 같은 결과를 표시합니다.
(3) 문자열 분할
문자열은 문자열 안에서 특정 문자(이를 테면 comma(,)와 같은)를 기준으로 array형태의 분리가 가능합니다.
Program.cs에서 comma로 분리된 도시이름을 포함한 단일 문자열 변수를 정의하고 이를 Split method를 통해 분리하는 문을 추가합니다. 이때 분리의 기준을 comma로 지정하며 반환된 문자열값의 array를 열거하도록 합니다.
string cities = "Seoul,Busan,Daegu,Daejeon,Jeju";
string[] citiesArray = cities.Split(',');
Console.WriteLine($"There are {citiesArray.Length} items in the array:");
foreach (string item in citiesArray)
{
Console.WriteLine(item);
}
예제를 실행하면 다음과 같은 결과를 볼 수 있습니다.
(4) 문자열의 일부 가져오기
문자열의 일부를 가져오는 방법 중의 하나로 IndexOf method를 사용할 수 있습니다. 해당 method는 문자열 안에서 지정한 문자 또는 문자열에 대한 index위치를 반환하는 9개의 overload를 가지고 있습니다.
Substring method는 2개의 다음과 같은 2개의 overload를 가집니다.
- Substring(startIndex, length) : startIndex로 시작하고 다음 length 문자만큼을 포함하는 문자열의 일부를 반환합니다.
- Substring(startIndex) : startIndex로 시작하고 그다음 끝까지 모든 문자열을 포함하는 문자열의 일부를 반환합니다.
Program.cs에서 사람의 이름을 저장하는 string변수를 추가합니다. 이때 성과 이름은 공백으로 구분합니다. 그리고 해당 공백의 위치를 찾아 성과 이름을 분리하며 다른 순서로도 재결합될 수 있도록 합니다.
string fullName = "hong seojun";
int indexOfTheSpace = fullName.IndexOf(' ');
string firstName = fullName.Substring(startIndex: 0, length: indexOfTheSpace);
string lastName = fullName.Substring(startIndex: indexOfTheSpace + 1);
Console.WriteLine($"Original: {fullName}");
Console.WriteLine($"Swapped: {lastName}, {firstName}");
예제를 실행하면 다음 결과를 볼 수 있습니다.
(5) 내용에서 문자열 확인
문자열에서는 특정 문자열로 시작하는지 혹은 끝나는지, 아니면 어떤 문자열을 포함하고 있는지를 확인할 수 있습니다.
Program.cs에서 문자열 저장을 위한 string변수를 추가하고 특정 문자열로 시작하는지와 포함하는지를 확인하는 문을 아래와 같이 작성합니다.
string company = "Microsoft";
bool startsWithM = company.StartsWith("M");
bool containsN = company.Contains("N");
Console.WriteLine($"Text: {company}");
Console.WriteLine($"Starts with M: {startsWithM}, contains an N: {containsN}");
예제를 실행하면 다음과 같은 결과를 표시할 것입니다.
(6) 문자열 결합, 형식화 그리고 기타
아래 표에서는 string에서 사용할 수 있는 몇 가지 member를 나타내고 있습니다.
Member | Description |
Trim, TrimStart, TrimEnd | 해당 method는 space나 tab 그리고 carriage return과 같은 공백문자를 제거합니다. |
ToUpper, ToLower | 대상 문자열을 전부 대문자 혹은 소문자로 변환합니다. |
Insert, Remove | 특정 문자열을 삽입하거나 제거합니다. |
Replace | 특정 문자열을 다른 문자열로 대체합니다. |
string.Empty | 빈문자열를 표시하기 위해 매번 2중쌍따옴표(")를 사용하는 대신 공백을 표현하는데 사용될 수 있습니다. |
string.Concat | 두 문자열 변수를 연결합니다. +연산자를 피연산자인 문자열 사이에 사용해도 동일한 결과를 얻을 수 있습니다. |
string.Join | 하나 또는 그 이상의 문자열을 각각의 문자열 변수 사이에 문자를 통해 연결합니다. |
string.IsNullOrEmpty | string변수가 null이거나 비어있는지를 확인합니다. |
string.IsNullOrWhitespace | string변수가 null이거나 공백인지를 확인합니다. 이때 공백은 tab, space, carriage return, line feed등을 포함합니다. |
string.Format | 형식화된 문자열변수를 출력하기 위한 보간문자열의 대체 method로 명명된 매개변수대신 위치를 사용합니다. |
상기 method 중 'string.'으로 표시된 method는 static method입니다. 즉, instance변수가 아닌 type에서부터 바로 호출될 수 있습니다.
Program.cs에서 문자열값의 배열을 가져와 Join method를 통해 분리 문자열을 사용하여 단일 문자열로 결하는 문을 추가합니다.
string cities = "Seoul,Busan,Daegu,Daejeon,Jeju";
string[] citiesArray = cities.Split(',');
string recombined = string.Join(" => ", citiesArray);
Console.WriteLine(recombined);
예제를 실행하면 다음과 같은 결과를 확인할 수 있습니다.
그리고 위치 매개변수와 보간 문자열 서식 구문을 사용해 같은 세 개의 변수를 두 번 표시하도록 하는 문을 추가합니다.
string fruit = "Apples";
int price = 2000;
DateTime when = DateTime.Today;
Console.WriteLine($"Interpolated: {fruit} cost {price:C} on {when:dddd}.");
Console.WriteLine(string.Format("string.Format: {0} cost {1:C} on {2:dddd}.", arg0: fruit, arg1: price, arg2: when));
예제를 실행하면 다음과 같은 결과를 표시합니다.
참고로 Console.WriteLine method는 string.Format와 동일한 서식을 지원하고 있기 때문에 좀 더 문을 간소화할 수 있습니다.
Console.WriteLine("WriteLine: {0} cost {1:C} on {2:dddd}.", arg0: fruit, arg1: price, arg2: when);
(7) 문자열을 효휼적으로 다루기
String.Concat method나 혹은 간단히 + 연산자를 사용하면 2개의 문자열을 결합해 새로운 문자열을 만들 수 있습니다. 하지만 이런 식의 문자열결합은 .NET이 memory에 완전히 새로운 문자열을 생성해야 하므로 좋지 않은 방법이라 할 수 있습니다.
물론 대게는 단순히 2개의 문자열을 결합하는 경우라면 큰 문제가 없을 테지만 반복문안에서 많은 수의 반복으로 문자열결합을 구현하는 경우라면 성능면에서 부정적인 영향이 올 수 있으며 memory낭비 역시 초래할 수 있습니다.
따라서 다수의 문자열을 많이 결합해야 하는 경우라면 StringBuilder type을 사용하는 것이 좋습니다. 이에 관한 내용은 아래 link을 참고해 주시기 바랍니다.
StringBuilder Class (System.Text) | Microsoft Learn
3. 정규 표현식을 통한 pattern matching
흔히 정규 표현식은 사용자로부터의 입력을 검증하는 데 사용되곤 합니다. 정규표현식은 아주 강력하지만 동시에 매우 복잡해질 수 있습니다. 거의 모든 programming언어가 정규표현식을 지원하고 있으며 이를 정의하기 위해 공통적인 특별한 문자들을 사용합니다.
표현식의 예를 만들어 보기 위해 csStudy08 solution에서 WorkingWithRegularExpressions이름의 Console App project를 생성합니다.
(1) 문자열로서 입력된 숫자 확인하기
숫자 입력을 검증하는 일반적인 예를 구현하는 것으로 시작해 보고자 합니다.
WorkingWithRegularExpressions project의 Program.cs에서 사용자에게 나이입력을 요청하고 입력된 값에서 숫자문자를 찾는 정규표현식을 아래와 같이 작성합니다.
using System.Text.RegularExpressions;
Console.Write("Enter your age: ");
string input = Console.ReadLine()!;
Regex ageChecker = new(@"\d");
if (ageChecker.IsMatch(input))
{
Console.WriteLine("Thank you!");
}
else
{
Console.WriteLine($"This is not a valid age: {input}");
}
위 예제에서는 아래 사항에 주목해야 합니다.
- @문자는 문자열 안에서 escape문자를 사용할 수 있는 기능을 끄는데, escape문자 앞에는 backslash가 붙습니다. 예를 들어 \t는 tab을 의미하며 \n은 new line을 의미합니다. 표현식을 사용할 때는 이 기능을 바꿔 사용해야 하므로 disable 해야 합니다.
- Escape문자가 @문자를 통해 비활성화되면 정규표현식으로 해석될 수 있습니다. 예를 들어 \d는 숫자를 의미하게 되는 것입니다. 이 부분에 관해서는 잠시 후 더 자세히 알아볼 것입니다.
예제를 실행하면 나이를 묻게 되고 숫자를 입력하면 다음과 같은 결과가 표시될 것입니다.
그런데 숫자가 아닌 다른 형태로 값을 입력하게 되면 결과는 달라지게 됩니다.
이번에는 숫자와 다른 문자를 섞어 입력해 봅니다.
정규표현식에서는 \d를 사용했습니다. 이는 숫자 하나를 의미합니다. 그러나 한자리 숫자 앞뒤에 무엇이 입력될 수 있는지는 명시하지 않았습니다. 해당 정규표현식을 풀어서 설명하면 '하나의 숫자를 입력하는 한 어떤 문자를 입력해도 상관없다.'가 됩니다.
정규표현식에서는 ^문자를 통해 시작되어야 하는 값을 나타낼 수 있고 $문자를 통해서는 끝나냐 하는 값을 나타낼 수 있습니다. 예제에서는 이들 기호문자를 사용해 숫자를 제외하고 입력된 값 앞뒤에 아무것도 없음을 아래와 같이 표시하도록 합니다.
Regex ageChecker = new(@"^\d+$");
이제 예제를 다시 실행하면 어떤 길이든지 0과 정수만 허용하게 됨을 알 수 있습니다.
(2) 정규 표현식 성능 향상
정규표현식 사용을 위한 .NET type은 .NET platform전역에서 사용될 수 있으며 다수의 app이 이를 내장하고 있습니다. 따라서 이들은 성능에 중요한 영향력을 가지고 있지만 Microsoft로 부터의 최적화에 대한 관심을 받지 못했습니다.
.NET 5 이후에 들어와서는 System.Text.RegularExpressions namespace가 최대의 성능을 이끌어 내기 위해 내부적으로 재작성되으며 IsMatch와 같은 method를 사용한 일반적인 정규표현식 benchmark에서는 5배 정도 빠르게 측정되었습니다. 게다가 이러한 성능적 혜택을 누리기 위해 기존에 작성되었던 code를 다시 바꿀 필요가 없습니다.
.NET 7에서 Regex class의 IsMatch method는 성능향상을 위한 ReadOnlySpan<char>을 전달할 수 있는 overload를 갖고 있습니다.
(3) 정규 표현식 문법
아래 표는 정규표현식에서 사용할 수 있는 일반적인 기호문자를 정리한 것입니다.
기호 | 의미 | 기호 | 의미 |
^ | 시작값 | $ | 끝값 |
\d | 숫자 | \D | 비숫자 |
\s | 공백 | \S | 비공백 |
\W | 단어 문자 | \W | 비단어 문자 |
[A-Za-z0-9] | 문자의 범위 | \^ | ^(caret) 문자 |
[aeiou] | 가능한 문자집합 | [^aeiou] | 비가능한 문자집합 |
. | 모든 단일문자 | \. | .(dot) 문자 |
추가적으로 아래는 정규표현식에서 이전 기호에 영향을 주는 일부 정규표현식 한정자를 정리한 것입니다.
기호 | 의미 | 기호 | 의미 |
+ | 하나 또는 그 이상 | ? | 하나 아니면 없음 |
{3} | 정확히 3개 | {3,5} | 3~5 |
{3,} | 최소3개 | {,3} | 3이상 |
(4) 정규 표현식 예제
아래 표에서는 정규식을 통한 몇 가지 예를 나타내고 있습니다.
표현식 | 의미 |
\d | 한자리 숫자 |
a | 문자 a |
cliel | 단어 cliel |
^cliel | cliel로 시작하는 단어 |
cliel$ | cliel로 끝나는 단어 |
^\d{2}$ | 정확히 2자리 숫자 |
^[0-9]{2}$ | 위와 동일, 정확히 2자리 숫자 |
^[A-Z]{4,}$ | ASCII 문자 set에서의 최소 4개의 영문 대문자 |
^[A-Za-z]{4,}$ | ASCII 문자 set에서의 최소 4개의 영문 대소문자 |
^[A-Z]{2}\d{3}$ | ASCII 문자 set에서의 2개 대문자와 3개의 숫자 |
^[A-Za-z\u00c0-\ u017e]+$ | ASCII 문자 set에서의 최소 1개의 대소문자 혹은 Unicode 문자 set에서의 European 문자 |
^d.g$ | 문자 d로 시작, 모든 문자, g로 끝나는 경우이므로 dig, dog처럼 d와 g사이에 단일문자 |
^d\.g$ | 문자 d로 시작, ., g로 끝나는 경우이므로 d.g에서만 일치 |
정규표현식은 거의 공통이므로 한번 사용한 정규표현식은 JavaScript나 Python과 같은 다른 언어에서 재사용이 가능합니다.
(5) 쉼표(,)로 구분된 문자열의 분리
위의 '문자열 분리'에서는 comma(,)로 구분된 문자열 변수에서 문자열을 어떻게 분리할 수 있을지에 대한 예제를 만들어본 바 있습니다. 그런데 아래와 같은 경우는 위에서 처럼 단순하게 처리할 수는 없는 형태입니다.
"aaa,bbb","ccc,ddd","eee,fff" |
위 문자열값은 각각에서 큰따옴표(")를 사용하고 있으며 해당 값들은 쉼표(,)로 분리할 필요가 있는지 여부를 확인하기 위해 사용할 것입니다. 문자열 분리를 위한 Split method가 존재하지만 해당 예제에서는 맞지 않으므로 정규표현식을 대신 사용해야 합니다.
아래 link는 해당 예제를 작성하는데 영향을 받게 된 Stack Overflow의 article로서 더 자세한 설명을 확인해 볼 수 있습니다.
string문자열 안에서 큰따옴표(")를 포함시키기 위해 이들 앞에 backslash를 붙여주거나 C# 11부터 도입된 'raw string literal'을 사용할 수 있습니다.
Program.cs에서 comma로 구분된 문자열값을 저장하는 변수를 추가하고 Split method를 통해 문자열을 나누어 봅니다.
string str = """
"aaa,bbb","ccc,ddd","eee,fff"
""";
Console.WriteLine($"Strings to split: {str}");
string[] attrStr = str.Split(',');
Console.WriteLine("Splitting with string.Split method:");
foreach (string s in attrStr)
{
Console.WriteLine(s);
}
그다음 해당 문자열을 다시 정규표현식을 통해 나누는 문을 아래와 같이 작성합니다.
Regex csv = new("(?:^|,)(?=[^\"]|(\")?)\"?((?(1)[^\"]*|[^,\"]*))\"?(?=,|$)");
MatchCollection colStr = csv.Matches(str);
Console.WriteLine("Splitting with regular expression:");
foreach (Match s in colStr)
{
Console.WriteLine(s.Groups[2].Value);
}
예제를 실행하면 다음과 같은 결과를 볼 수 있을 것입니다.
(6) 정규표현식 구문강조 사용하기
Visual Studio 2022를 사용한다면 위에서 정규표현식을 사용하고자 생성자값으로 문자열을 전달할 때 색상으로 구문이 강조되어 있음을 확인할 수 있습니다.
일반적인 문자열값을 처리할 때 적용되지 않던 색상 구문 강조가 정규 표현식을 사용할 때 적용된 이유는 무엇일까?
그 이유를 알아보기 위해 생성자에서 mouse오른쪽 button을 click 한 뒤 'Go To Implementation'항목을 선택합니다. 그러면 pattern 문자열 매개변수에 Regex 상수값이 전달된 StringSyntax 이름의 attribute가 적용되어 있음을 알 수 있습니다.
/// <summary>
/// Creates a regular expression object for the specified regular expression.
/// </summary>
public Regex([StringSyntax(StringSyntaxAttribute.Regex)] string pattern) :
this(pattern, culture: null)
{
}
여기서 다시 StringSyntax attribute에 mouse오른쪽 click한뒤 'Go To Implementation'항목을 선택합니다. 결과를 보면 Regex를 포함해 선택가능한 12개의 인식된 문자열 구문형식이 존재함을 알 수 있습니다.
public const string CompositeFormat = nameof(CompositeFormat);
/// <summary>The syntax identifier for strings containing date format specifiers.</summary>
public const string DateOnlyFormat = nameof(DateOnlyFormat);
/// <summary>The syntax identifier for strings containing date and time format specifiers.</summary>
public const string DateTimeFormat = nameof(DateTimeFormat);
/// <summary>The syntax identifier for strings containing <see cref="Enum"/> format specifiers.</summary>
public const string EnumFormat = nameof(EnumFormat);
/// <summary>The syntax identifier for strings containing <see cref="Guid"/> format specifiers.</summary>
public const string GuidFormat = nameof(GuidFormat);
/// <summary>The syntax identifier for strings containing JavaScript Object Notation (JSON).</summary>
public const string Json = nameof(Json);
/// <summary>The syntax identifier for strings containing numeric format specifiers.</summary>
public const string NumericFormat = nameof(NumericFormat);
/// <summary>The syntax identifier for strings containing regular expressions.</summary>
public const string Regex = nameof(Regex);
/// <summary>The syntax identifier for strings containing time format specifiers.</summary>
public const string TimeOnlyFormat = nameof(TimeOnlyFormat);
/// <summary>The syntax identifier for strings containing <see cref="TimeSpan"/> format specifiers.</summary>
public const string TimeSpanFormat = nameof(TimeSpanFormat);
/// <summary>The syntax identifier for strings containing URIs.</summary>
public const string Uri = nameof(Uri);
/// <summary>The syntax identifier for strings containing XML.</summary>
public const string Xml = nameof(Xml);
WorkingWithRegularExpressions project에서 Strings. cs라는 이름의 class file을 추가한 뒤 2개의 문자열 상수를 정의하는 문을 작성합니다.
partial class Program
{
const string digitsOnlyText = @"^\d+$";
const string commaSeparatorText = "(?:^|,)(?=[^\"]|(\")?)\"?((?(1)[^\"]*|[^,\"]*))\"?(?=,|$)";
}
Visual Studio 2022에서 위 예제를 작성하면 아직까지는 정규식에 색상 구문 강조가 적용되지 않습니다.
Program.cs에서 literal string을 아래와 같이 digits-only 정규 표현식에 대한 string constant로 바꾸고
Regex ageChecker = new(digitsOnlyText);
WorkingWithText project의 comma 분리 정규표현식에서도 아래와 같이 string constant로 바꾸어 줍니다.
Regex csv = new(commaSeparatorText);
Strings.cs에서는 StringSyntax attribute를 위한 namespace를 추가하고 string상수 둘다에 StringSyntax attribute를 적용합니다.
partial class Program
{
[StringSyntax(StringSyntaxAttribute.Regex)]
const string digitsOnlyText = @"^\d+$";
[StringSyntax(StringSyntaxAttribute.Regex)]
const string commaSeparatorText = "(?:^|,)(?=[^\"]|(\")?)\"?((?(1)[^\"]*|[^,\"]*))\"?(?=,|$)";
}
이어서 날짜의 서식화를 위한 또 다른 상수를 아래와 같이 추가하고
[StringSyntax(StringSyntaxAttribute.DateTimeFormat)]
const string fullDateTime = "";
fullDateTime문자열 안에 'd'를 typing 하면 IntelliSense가 작동됨을 알 수 있습니다.
fullDateTime변수에 'yyyy-MM-dd'형식의 날짜 format을 지정하고 digitsOnlyText의 끝에는 \문자를 추가합니다. 이때 IntelliSense를 통해 올바른 정규표현식을 입력하는데 도움을 받을 수 있습니다.
[StringSyntax] attribute는 .NET 7에서 새롭게 도입된 기능으로 이를 인식하는 것은 code editor의 몫입니다. .NET 7에는 이뿐만 아니라 해당 attribute에 적용할 수 있는 350개 이상의 매개변수, 속성, field를 갖고 있습니다.
(7) Source generator를 통한 정규표현식의 성능향상
문자열값 혹은 문자열상수를 Regex의 생성자로 전달하면 class는 문자열을 분석하여 정규표현식 번역기에 의해 효휼적으로 실행될 수 있도록 최적화된 방식으로 표현식을 나타내는 내부 tree구조로 변환합니다.
정규 표현식은 RegexOption을 지정함으로써 아래와 같이 compile 할 수 있습니다.
Regex ageChecker = new(digitsOnlyText, RegexOptions.Compiled);
그런데 compiling은 정규표현식의 초기생성 속도를 늦추는 부정적인 효과를 가지고 있습니다. 번역기가 실행할 tree구조가 생성되고 나면 compiler는 이 tree를 IL code로 변환하고 IL code를 native code로 JIT compile 해야 합니다. 정규표현식을 몇 번만 실행시킬 거라면 이것을 굳이 compile 할 필요가 없을 것입니다.
.NET 7에서는 [GeneratedRegex] attribute를 Regex를 반환하는 partial method에 적용하면 이를 인식하는 정규표현식에 대한 source generator를 도입하였는데 이는 정규표현식에 대한 logic을 구현하는 method의 구현을 생성합니다.
WorkingWithRegularExpressions project에서 Regexs.cs라는 새로운 class file을 생성하고 아래와 같이 partial method를 정의합니다.
partial class Program
{
[GeneratedRegex(digitsOnlyText, RegexOptions.IgnoreCase)]
private static partial Regex DigitsOnly();
[GeneratedRegex(commaSeparatorText, RegexOptions.IgnoreCase)]
private static partial Regex CommaSeparator();
}
그리고 Program.cs에서 아래와 같이 digits-only 정규표현식을 반환하는 partial method를 호출하도록 생성자를 바꿔주고
Regex ageChecker = DigitsOnly();
comma 구분자 정규 표현식을 반환하는 partial method를 호출하도록 하는 생성자도 아래와 같이 바꿔줍니다.
Regex csv = CommaSeparator();
여기에서 각 partial method에 mouse pointer를 올려보면 표현식에 대한 동작을 설명하는 tooltip을 볼 수 있습니다.
그리고 DigitsOnly partial method를 mouse 오른쪽 click 하여 'Go To Definition'을 click 해 보면 자동으로 생성된 partial method의 구현체를 확인해 볼 수 있습니다.
예제를 실행하고 기존의 동작과 같은지를 확인합니다.
아래 link에서는 .NET 7에서 정규 표현식의 구현에 대한 더 많은 내용을 확인할 수 있습니다.
Regular Expression Improvements in .NET 7 - .NET Blog (microsoft.com)
4. Collection에서 다중 개체 저장하기
Data와 관련된 또 다른 가장 일반적인 type으로는 collection을 들 수 있습니다. Collection은 다중값을 저장하는 일반적인 type입니다.
Collection은 memory에 있는 data구조로서 모든 collection이 일부 기능을 공유하고 있기는 하지만 나름대로의 방식으로 다중 item을 관리할 수 있습니다.
아래 표는 .NET에서 collection을 사용하기 위한 가장 일반적인 type을 나타낸 것입니다.
Namespace | Example type(s) | Description |
System.Collections | IEnumerable, IEnumerable<T> | Collection에서 사용되는 Interface와 기반 class |
System.Collections.Generic | List<T>, Dectionary<T>, Queue<T>, Stack<T> | .NET Framework 2.0과 함께 C# 2.0에서 도입된 것으로 generic type 매개변수를 통해 원하는 type을 지정할 수 있는 collection (더 안전하고, 더 빠르며, 더 효휼적인) |
System.Collections.Concurrent | BlockingCollection, ConcurrentDictionary, ConcurrentQueue | Multithread scenario에서 사용하기에 안전한 collection |
System.Collections.Immutable | ImmutableArray, ImmutableDictionary, ImmutableList, ImmutableQueue | 본래 collection의 content가 절대 바뀌지 않는다는 scenario를 위해 설계된 것으로 다만 새로운 instance를 생성함으로서 기존의 collection을 변경할 수 있음 |
(1) 모든 collection에서의 공통기능
모든 collection은 ICollection interface를 구현하고 있고 때문에 얼마나 많은 개체가 존재하는지를 의미하는 Count속성을 가지고 있습니다.
public interface ICollection : IEnumerable
{
int Count { get; }
bool IsSynchronized { get; }
object SyncRoot { get; }
void CopyTo(Array array, int index);
}
예를 들어 한 학급의 학생에 대한 collection이 존재한다면 다음과 같이 학생의 수를 구할 수 있습니다.
int howMany = students.Count;
또한 모든 collection은 IEnumerable interface를 구현합니다. 때문에 foreach문을 통해 각 요소를 열거할 수 있습니다. 그리고 이를 위해서는 IEnumerator를 구현하는 개체를 반환하는 GetEnumerator method를 가져야 하며 반환된 개체는 collection을 탐색할 수 있는 MoveNext와 Reset method 그리고 collection에서 현재 item을 가지고 있는 Current 속성을 가져야 합니다.
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
public interface IEnumerator
{
object Current { get; }
bool MoveNext();
void Reset();
}
이렇게 되면 students collection에서 각 개체에 대해 아래와 같은 동작을 수행할 수 있습니다.
foreach (Student s in students)
{
}
개체 기반 collection interface뿐만 아니라 collection에 저장되는 type을 정의하는 generic type generic interface와 class도 존재합니다.
public interface ICollection<T> : IEnumerable<T>, IEnumerable
{
int Count { get; }
bool IsReadOnly { get; }
void Add(T item);
void Clear();
bool Contains(T item);
void CopyTo(T[] array, int index);
bool Remove(T item);
}
(2) Collection의 가용성을 지정하여 성능 향상하기
.NET 1.1부터 StringBuilder와 같은 type은 EnsureCapacity라는 이름의 method를 가지고 있어서 이를 통해 array를 저장하는 size를 미리 지정할 수 있었고 따라서 string의 마지막 size를 예상할 수 있었습니다. 이 것은 문자가 추가됨으로서 array의 size를 반복적으로 증가시키지 않아도 되므로 성능에 도움이 될 수 있습니다.
.NET 6부터 List<T>, Queue<T>, Stack<T>와 같은 collection은 같은 역할로 EnsureCapacity이름의 method를 가지게 되었으며 아래와 같이 사전 size를 지정할 수 있게 되었습니다.
List<string> names = new();
names.EnsureCapacity(10000);
(3) Collection의 종류
Collection은 사용하고자 하는 목적에 따라 lists, dictionaries, queues, sets 등 선택가능한 여러 가지가 존재합니다.
● Lists
IList<T>를 구현하는 List는 정렬된 collection입니다.
[DefaultMember("Item")] // aka this indexer
public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable
{
T this[int index] { get; set; }
int IndexOf(T item);
void Insert(int index, T item);
void RemoveAt(int index);
}
IList<T>는 ICollection<T>로 부터 파생되므로 Count속성을 가지고 있으며 collection끝에 item을 추가하기 위한 Add method와 특정한 위치에 item을 추가하는 Insert method, 지정한 위치에 item을 제거하는 RemoveAt method를 가지고 있습니다.
따라서 List는 수동적으로 collection의 item 순서를 조정할 수 있습니다. List의 각 item은 자동적으로 할당된 고유한 index(또는 위치값)를 가지고 있으며 또한 T에 의해 어떠한 type으로도 item이 정의될 수 있고 필요하다면 중복으로 item을 저장시킬 수 있습니다. Index는 int type으로서 0부터 시작하므로 list의 첫 번째 item의 index값은 0이 됩니다.
Index | Item |
0 | apple |
1 | banana |
2 | orange |
만약 위 상태에서 banana와 orange사이에 lemon이라는 새로운 item이 삽입되면 orange의 index는 자동적으로 증가됩니다. 따라서 item의 index는 다른 item의 삽입이나 제거로 인해 바뀔 수 있음에 주의해야 합니다.
Index | Item |
0 | apple |
1 | banana |
2 | lemon |
3 | orange |
때로는 Array가 더 나은 상황임에도 굳이 List<T>나 다른 collection을 사용하는 경우가 있습니다. 그러나 이는 좋지 않은 습관입니다. Data의 size가 instance화 된 이후에 바뀌는 않는 경우라면 array를 사용하는 것이 좋습니다.
● Dictionary
Dictionary는 각 값(또는 개체)이 고유한 하위 값을 가질 수 있으며 이는 collection에서 빠르게 값을 찾는 key로 사용될 수 있습니다. 여기서 key는 고유해야 합니다. 예를 들어 사람에 대한 list를 저장하는 경우라면 주민등록번호를 key로서 사용할 수 있을 것입니다. Dictionary는 Python과 Java와 같은 다른 언어에서는 hashmaps라고도 합니다.
Key는 실제 사전에서 index항목과 같이 생각할 수 있습니다. 단어(즉, key)는 정렬된 상태로 유지되므로 빠르게 단어의 정의를 찾을 수 있게 하며 만약 manatee단어에 대한 정의를 찾는다고 하면 사전의 중간쯤부터 시작할 수 있을 것입니다. 왜냐하면 문자 M은 alphabet의 중간쯤에 위치해 있기 때문입니다.
Programming에서 Dictionary는 무엇인가를 찾을 때 위와 비슷하게 동작할 수 있습니다. Dictionary는 IDictionary<TKey, TValue> interface를 구현합니다.
[DefaultMember("Item")] // aka this indexer
public interface IDictionary<TKey, TValue> : ICollection<KeyValuePair<TKey, TValue>>, IEnumerable<KeyValuePair<TKey, TValue>>, IEnumerable
{
TValue this[TKey key] { get; set; }
ICollection<TKey> Keys { get; }
ICollection<TValue> Values { get; }
void Add(TKey key, TValue value);
bool ContainsKey(TKey key);
bool Remove(TKey key);
bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value);
}
Dictionary의 item은 값 type인 KeyValuePair<TKey, TValue> struct의 instance로서 TKey는 key의 type이며 TValue는 value의 type입니다.
public readonly struct KeyValuePair<TKey, TValue>
{
public KeyValuePair(TKey key, TValue value);
public TKey Key { get; }
public TValue Value { get; }
[EditorBrowsable(EditorBrowsableState.Never)]
public void Deconstruct(out TKey key, out TValue value);
public override string ToString();
}
예를 들어 Dictionary<string, Person>는 key로서 string을, 값(value)으로서 Person instance를 사용하는 것입니다. 아래 예제 Dictionary<string, string>은 값과 key로서 둘 다 string을 사용합니다.
key | value |
801201-1 | 홍길동 |
750405-2 | 홍길남 |
820712-1 | 홍길석 |
● Stack
Stack는 LIFO(last-in, first-out) 동작을 구현하기에 적합합니다. 따라서 stack에서는 전체 item을 열거할 수는 있지만 stack의 가장 상위에 있는 하나의 item에 대한 직접적인 접근 및 제거만이 가능합니다. Stack에 대한 두 번째나 세 번째 등의 접근 불가능합니다.
Stack의 가장 대표적인 사례로는 word process를 들 수 있습니다. Stack을 통해 우리가 한 동작을 순서대로 기억하고 있다가 Ctrl + Z key를 누르게 되면 stack에서 마지막 동작을 읽어 되돌리게 되는 것입니다.
● Queue
Queue는 FIFO(first-in, first-out)동작을 구현하기에 적합합니다. 따라서 queue에서는 전체 item을 열거할 수는 있지만 queue의 가장 앞에 있는 item에 대한 직접적인 접근및 제거만이 가능하며 두번째나 세번째등의 접근은 불가능합니다.
Queue의 가장 대표적인 사례로는 은행에서 줄을 서 있는 사람들처럼 들어온 순서대로 작업 item을 처리할 때 queue를 사용한 경우를 들 수 있습니다.
.NET 6에서는 queue의 각 item에 position과 우선순위값이 할당된 PriorityQueue를 도입하였습니다.
● Set
Set은 2개의 collection사이에 집합 연산을 수행합니다. 예를 들어 과일에 대한 2개의 collection을 가지고 있을 때 2개 collection모두에서 공통적으로 속해있는(집합 사이에 교차하는) 과일의 이름을 알고 싶은 경우 set을 사용할 수 있습니다. Set에서 item은 고유해야 합니다.
● Collection method
각 collection은 item을 추가하거나 삭제할 때 사용하는 일련의 method가 다르며 아래 표는 collection별로 이러한 method를 정리한 것입니다.
Collection | Add method | Remove method | Description |
List | Add, Insert | Remove, RemoveAt | List는 정렬되어 있으므로 item은 int형식의 index position을 갖고 있습니다. Add는 list의 끝에 새로운 item을 추가하며 Insert는 지정한 위치에 새로운 item을 추가하게 됩니다. |
Dictionary | Add | Remove | Dictionary는 정렬되어 있지 않으므로 item또한 index position을 갖고 있지 않습니다. ContainsKey method를 호출하면 해당 key가 사용되었는지 여부를 확인할 수 있습니다. |
Stack | Push | Pop | Push method는 새로운 item을 stack의 상위에 추가합니다. 따라서 첫번째 item은 맨 마지막에 있게 됩니다. Item이 Pop을 통해 제거될때는 stack의 가장 상위의 것이 제거됩니다. Peek method를 호출하면 해당 값을 삭제하지 않고도 확인할 수 있습니다. |
Queue | Enqueue | Dequeue | Enqueue method를 통해 item이 추가될때는 항상 queue의 끝에 추가됩니다. 따라서 첫번째 item은 queue의 앞에 있게 됩니다. Dequeue method를 통해 item이 제거될때는 queue의 가장 앞에 있는 item을 제거합니다. Peek method를 호출하면 해당 값을 삭제하지 않고도 확인할 수 있습니다. |
(4) List 사용하기
csStudy08 solution에서 WorkingWithCollections이름의 Console App project를 추가하고 Helpers.cs class를 아래와 같이 추가합니다.
partial class Program
{
static void Output(string title, IEnumerable<string> collection)
{
Console.WriteLine(title);
foreach (string item in collection)
{
Console.WriteLine($" {item}");
}
}
}
Program.cs에서는 기존의 문을 모두 삭제하고 list를 정의하고 사용하는 일반적인 방법을 나타내는 문을 아래와 같이 추가합니다.
List<string> cities = new();
cities.Add("Daegu");
cities.Add("Seoul");
cities.Add("Busan");
Output("Initial list", cities);
Console.WriteLine($"The first city is {cities[0]}.");
Console.WriteLine($"The last city is {cities[cities.Count - 1]}.");
cities.Insert(0, "Jeju");
Output("After inserting Sydney at index 0", cities);
cities.RemoveAt(1);
cities.Remove("Seoul");
Output("After removing two cities", cities);
예제에서 cities list는 아래 2가지 다른 방식으로도 정의될 수 있습니다.
List<string> cities = new() { "London", "Paris", "Milan" };
List<string> cities = new();
cities.AddRange(new[] { "London", "Paris", "Milan" });
예제를 실행하면 다음과 같은 결과가 표시될 것입니다.
(5) Dictionary 사용하기
Program.cs에서는 Dictionary를 정의하고 사용하며 단어를 찾는 등의 일반적인 방법을 나타내는 문을 아래와 같이 추가합니다.
Dictionary<string, string> keywords = new();
keywords.Add(key: "int", value: "32-bit integer data type");
keywords.Add("long", "64-bit integer data type");
keywords.Add("float", "Single precision floating point number");
Output("Dictionary keys:", keywords.Keys);
Output("Dictionary values:", keywords.Values);
Console.WriteLine("Keywords and their definitions");
foreach (KeyValuePair<string, string> item in keywords)
{
Console.WriteLine($" {item.Key}: {item.Value}");
}
string key = "long";
Console.WriteLine($"The definition of {key} is {keywords[key]}");
예제에서 첫 번째 Add method는 명명된 매개변수를 사용한 추가방식을 나타내고 있습니다. 또한 Dictionary는 아래와 같은 2가지 다른 방식을 통해 정의될 수 있습니다.
Dictionary<string, string> keywords = new()
{
{ "int", "32-bit integer data type" },
{ "long", "64-bit integer data type" },
{ "float", "Single precision floating point number" },
};
Dictionary<string, string> keywords = new()
{
["int"] = "32-bit integer data type",
["long"] = "64-bit integer data type",
["float"] = "Single precision floating point number",
};
특히 상기예제에서 마지막 comma(,)는 생략할 수 있습니다.
(6) Queue 사용하기
Program.cs에서는 queue를 정의하고 사용하는 일반적인 방법을 나타내는 문을 아래와 같이 추가합니다. 예제는 Queue를 통해 음식점에서 손님에게 순서대로 주문한 음식을 제공하는 경우를 나타낸 것입니다.
Queue<string> foods = new();
foods.Enqueue("제육볶음");
foods.Enqueue("된장찌개");
foods.Enqueue("김치찌개");
foods.Enqueue("백반정식");
foods.Enqueue("라면");
Output("Initial queue from front to back", foods);
string served = foods.Dequeue();
Console.WriteLine($"Served: {served}.");
served = foods.Dequeue();
Console.WriteLine($"Served: {served}.");
Output("Current queue from front to back", foods);
Console.WriteLine($"{foods.Peek()} is next in line.");
Output("Current queue from front to back", foods);
예제를 실행하면 다음과 같은 결과를 볼 수 있습니다.
Helpers.cs의 partial Program class에서 OutputPQ라는 이름의 정적 method를 아래와 같이 추가합니다.
static void OutputPQ<TElement, TPriority>(string title, IEnumerable<(TElement Element, TPriority Priority)> collection)
{
Console.WriteLine(title);
foreach ((TElement, TPriority) item in collection)
{
Console.WriteLine($" {item.Item1}: {item.Item2}");
}
}
OutputPQ는 generic임에 주목합니다. 이를 통해 collection에 전달되는 tuple에서 사용할 2개의 type을 지정할 수 있습니다.
Program.cs에서는 priority queue를 정의하고 사용하는 일반적인 방법을 나타내는 문을 아래와 같이 추가합니다.
PriorityQueue<string, int> foods = new();
foods.Enqueue("제육볶음", 1);
foods.Enqueue("된장찌개", 4);
foods.Enqueue("김치찌개", 3);
foods.Enqueue("백반정식", 2);
foods.Enqueue("라면", 1);
OutputPQ("Initial queue from front to back", foods.UnorderedItems);
Console.WriteLine($"{foods.Dequeue()} has been served.");
Console.WriteLine($"{foods.Dequeue()} has been served.");
OutputPQ("Current queue :", foods.UnorderedItems);
Console.WriteLine($"{foods.Dequeue()} has been served.");
Console.WriteLine("Adding Mark to queue with priority 2");
foods.Enqueue("오무라이스", 2);
Console.WriteLine($"{foods.Peek()} will be next to be served.");
OutputPQ("Current queue :", foods.UnorderedItems);
예제에서 Enqueue method의 두 번째 숫자가 우선순위에 해당합니다. 1이 가장 높은 우선순위를 4가 가장 낮은 우선순위가 됩니다.
예제를 실행하면 다음과 같은 결과를 확인할 수 있습니다.
(7) Collection 정렬
List<T> class에서는 Sort method를 호출함으로써 수동적으로 정렬할 수 있습니다(단, 각 item의 index는 바뀔 수 있습니다.). 문자열값 혹은 다른 내장 type의 list를 수동적으로 정렬하는 것은 별도의 구현이 필요 없이 작업이 가능하지만 자신만의 type으로 collection을 생성한 경우라면 해당 type은 IComparable이름의 interface를 구현해야 합니다. 이에 대한 자세한 내용은 아래 글에서 확인하실 수 있습니다.
[.NET/C#] - [C# 11 과 .NET 7] 6. Interface와 Class상속
Stack<T>또는 Queue<T> collection은 근본적인 특성상 정렬이 불가능하며 Dictionary 혹은 set에서는 정렬이 가능할 수 있습니다.
하지만 때로는 자동적으로 정렬된 collection, 즉, item을 추가하거나 삭제하면 그것대로 순서대로 정렬된 item을 유지하는 것을 사용하는 것이 유용할 때가 있습니다.
이를 위해 선택가능한 여러 자동정렬 collection들이 존재합니다. 이들 정렬된 collection의 차이는 미묘하지만 application에서 사용될 때 필요한 memory와 성능에는 큰 영향이 있을 수 있습니다. 따라서 필요에 따라 가장 적합한 것을 선택할 수 있도록 신중해질 필요가 있습니다.
선택가능한 자동정렬 collection에는 다음과 같은 것들이 있습니다.
Collection | 설명 |
SortedDictionary<TKey, TValue> | key에 의해 정렬된 key/값 쌍의 collection입니다. |
SortedList<TKey, TValue> | key에 의해 정렬된 key/값 쌍의 collection입니다. |
SortedSet<T> | 순서대로 정렬된 상태를 유지하는 고유한 개체의 collection입니다. |
(8) 더 전문화된 collection
위의 collection 외 더 특별한 상황을 위한 다른 collection들이 존재합니다.
● Bit 값을 사용하는 compact array
System.Collections.BitArray collection은 bool형식의 bit값에 대한 array를 관리하며 true라면(값이 1이라면) bit가 on이고 false라면(값이 0이라면) bit가 off임을 의미합니다.
● Efficient list
System.Collections.Generics.LinkedList<T> collection은 이중연결 list를 나타내는 것으로 모든 item은 전의 item과 후의 item에 대한 참조를 갖고 있습니다. 이러한 구조로 list의 중간쯤에 자주 item에 대한 insert나 remove가 이루어지는 상황이라면 List<T>와 비교해 더 우월한 성능을 나타내며 구조의 특성상 LinkedList<T>에서 item은 memory에서 재정렬될 필요가 없습니다.
(9) Immutable collection
Immutable collection은 말 그대로 불변의 collection을 생성합니다. 다시 말해 collection의 member는 바뀔 수 없고 따라서 추가나 삭제도 불가능합니다.
Immutable collection은 System.Collections.Immutable namespace를 import 하게 되면 IEnumerable<T>를 구현하는 모든 collection은 여섯 개의 확장 method가 주어지는데 이를 통해 immutable list나 dictionary, hash, set 등으로 변환하는 것으로 사용할 수 있습니다.
WorkingWithCollections의 Program.cs에서 System.Collections.Immutable namespace를 import 하고 cities list를 immutable list로 변환한 뒤 새로운 item을 아래와 같이 추가하도록 합니다.
List<string> cities = new();
cities.Add("Daegu");
cities.Add("Seoul");
cities.Add("Busan");
ImmutableList<string> immutableCities = cities.ToImmutableList();
ImmutableList<string> newList = immutableCities.Add("Jeju");
Output("Immutable list of cities:", immutableCities);
Output("New list of cities:", newList);
예제를 실행하면 다음과 같은 결과를 볼 수 있습니다.
예제에서 cities의 immutable list는 바뀌지 않음에 주목해야 합니다. Add method를 통해 item을 추가하게 되면 자신을 바꾸는 것이 아닌 기존의 것에서 새로운 것이 추가된 상태의 list가 새롭게 반환됩니다.
성능향상을 위해 많은 application에서는 공통적으로 접근되는 개체의 공유복사본을 cache에 복사하고 여러 thread에서의 안정적인 동작이 필요한 경우 이를 immutable로 만들거나 concurrent collection type을 사용하게 됩니다. 이와 관련해서는 아래 link에서 자세한 내용을 확인할 수 있습니다.
System.Collections.Concurrent Namespace | Microsoft Learn
(10) Collection에 대한 모범사례
Collection을 처리해야 하는 method를 만들어야 한다고 가정해 보면 예를 들어 최대한의 유용성을 활용하기 위해 입력 매개변수를 IEnumerable<T>로 선언하여 method를 generic으로 만들 수 있습니다.
void ProcessCollection<T>(IEnumerable<T> collection)
{
}
이렇게 하면 여기에 int나 string 또는 사용자 type 등 IEnumerable<T>를 구현하는 다른 모든 type을 포함하는 list, queue, stack과 같은 array를 전달할 수 있습니다. 그러나 해당 method로 모든 collection을 전달하고자 하는 이런 유연성은 꽤 많은 처리비용을 요구할 수 있습니다.
IEnumerable<T>로 하면 이에 대한 성능저하의 문제점으로 개체를 heap에 할당해야 하는 것으로 기인합니다. 이러한 memory할당을 피하려면 위의 method에서 아래와 같이 구체적인 type을 명시해야 합니다.
void ProcessCollection<T>(List<T> collection)
{
}
이렇게 하면 IEnumerator<T>에서 참조 type을 반환하는 GetEnumerator method대신 구조체를 반환하는 List<T>의 GetEnumerator method를 사용하게 됩니다. 이것만으로 2~3배 정도의 성능상향을 기대할 수 있으며 그만큼 memory로 더 적게 사용합니다. 성능과 관련된 다른 모든 권장사항들이 그렇듯이 실제 code를 test 함으로써 성능에 관한 이점을 확인해야 합니다.
5. Span, Index 그리고 range
.NET Core 2.1에서 Microsoft의 목표 중 하나는 성능 및 resource의 사용을 개선하는 것이었으며 이에 대한 핵심 .NET 기능이 Span<T> type입니다.
(1) Span을 통한 memory 효율성 증가
Array를 다룰 때 어떤 경우에는 기존의 것에서 일부만 따로 떼어낸 새로운 복사본이 필요할 때가 있습니다. 그런데 실제 이와 같은 복사본을 생성하는 것은 memory안에 동일한 같은 개체를 만들게 되므로 그리 효휼적이라고 할 수는 없습니다.
따라서 만약 기존 array에서 전체 혹은 부분적으로 동일한 array를 대신 사용해야 한다면 본래 array에 대한 거울과 같은 span을 사용할 수 있습니다. Memory사용율과 성능면에서 더 효율적인 span은 오로지 array에 대해서만 사용할 수 있으며 memory가 연속되어야 하므로 collection에서는 사용할 수 없습니다.
(2) Index type을 통한 위치 식별
C# 8.0에서는 array안에서 item의 위치와 2개의 index를 사용해 item의 범위를 식별하기 위한 2가지 기능을 도입했습니다.
list의 개체는 indexer에 int값을 전달함으로써 다음과 같이 접근할 수 있습니다.
int index = 3;
Person p = people[index];
char letter = name[index];
Index type은 아래와 같이 사용할 수 있으며 position을 식별하기 위한 공식적인 방법이고 또한 아래와 같이 끝에서부터 계산할 수 있는 fromEnd를 사용할 수 있습니다.
Index i1 = new(value: 3);
Index i2 = 3;
Index i3 = new(value: 5, fromEnd: true);
Index i4 = ^5;
index를 정의하기 위해서 예제와 같이 명시적으로 숫자를 사용하거나 new(value: 3)과 같이 사용할 수 있습니다. 이때 index는 시작부터 3번째를 의미합니다.
끝에서 부터 counting 되도록 하는 경우 fromEnd를 사용하거나 ^(caret) 문자를 사용합니다.
(3) Range type으로 범위 식별하기
Range type은 범위의 시작과 끝을 나타내기 위해 Index값을 사용하며 생성자 혹은 정적 method를 사용해 생성합니다.
Range r1 = new(start: new Index(3), end: new Index(7));
Range r2 = new(start: 3, end: 7);
Range r3 = 3..7;
Range r4 = Range.StartAt(3);
Range r5 = 3..;
Range r6 = Range.EndAt(3);
Range r7 = ..3;
기본적으로 range type은 위 예제에서 첫 번째와 두 번째같이 생성자를 통해 생성할 수 있습니다. 세 번째는 C# 8부터 사용가능한 방식이며 네번째는 정적 method를 사용한 것으로 세번째 index부터 끝까지를 의미합니다. 다섯번째는 네번째와 의미가 같고 여섯번째는 끝에서 부터 세번째 index를 의미합니다. 일곱 번째는 여섯 번째와 같습니다.
확장 method는 문자열값(내부적으로 char 배열을 사용), int array 그리고 span에 추가되어 범위를 더 쉽게 다룰 수 있습니다. 이들 확장 method는 매개변수로 범위를 수용하고 Span<T>를 반환함으로써 memory를 더 효휼적으로 사용할 수 있도록 합니다.
(4) Indexe, range, 그리고 span사용하기
Span을 반환하는 index와 range는 아래와 같이 사용할 수 있습니다.
csStudy08 solution에서 WorkingWithRanges이름의 Console app project를 생성하고 Program.cs에서 기존의 문을 모두 삭제한 뒤 string type의 substring method에서 범위를 지정하여 이름을 추출하고 비교하는 문을 추가합니다.
string name = "Gildong hong";
//성과 이름의 길이를 확인합니다.
int lengthOfFirst = name.IndexOf(' ');
int lengthOfLast = name.Length - lengthOfFirst - 1;
//성과 이름을 분리해 가져옵니다.
string firstName = name.Substring(startIndex: 0, length: lengthOfFirst);
string lastName = name.Substring(startIndex: name.Length - lengthOfLast, length: lengthOfLast);
Console.WriteLine($"First name: {firstName}, Last name: {lastName}");
위의 예제를 span을 사용해 구현하면 다음과 같이 할 수 있습니다.
ReadOnlySpan<char> nameAsSpan = name.AsSpan();
ReadOnlySpan<char> firstNameSpan = nameAsSpan[0..lengthOfFirst];
ReadOnlySpan<char> lastNameSpan = nameAsSpan[^lengthOfLast..^0];
Console.WriteLine("First name: {0}, Last name: {1}", arg0: firstNameSpan.ToString(), arg1: lastNameSpan.ToString());
예제를 실행하면 결과를 다음과 같을 것입니다.
5. Network Resource
외부와의 통신이 필요한 application이라면 network와 관련된 여러 가지 작업이 필요합니다. 아래 표는 그런 경우 사용가능한 type을 나열한 것입니다.
Namespace | Type | Description |
System.Net | Dns, Uri, Cookie, WebClient, IPAddress | DNS server, URIs, IP address관련및 기타 |
System.Net | FtpStatusCode, FtpWebRequest, FtpWebResponse | FTP server 관련 |
System.Net | HttpStatusCode, HttpWebRequest, HttpWebResponse | HTTP server와 관련된 것으로서 website와 service등을 의미 |
System.Net.Http | HttpClient, HttpMethod, HttpRequestMessage, HttpResponseMessage | HTTP server와 관련된 것으로서 website와 service등을 의미 |
System.Net.Mail | Attachment, MailAddress, MailMessage, SmtpClient | Mail을 발송하는 SMTP server관련 |
System.Net .NetworkInformation | IPStatus, NetworkChange, Ping, TcpStatistics | 저수준 network protocol관련 |
(1) URI, DNS, IP 주소
Network resource를 사용하는 예를 작성하기 위해 csStudy08 solution에서 WorkingWithNetworkResources이름의 Console App을 생성하고 Program.cs에서 기존의 문을 모두 삭제한 뒤 아래의 namespace를 import 합니다.
using System.Net;
그리고 Program.cs에서 사용자에게 website주소의 입력을 요청하고 주소가 입력되면 Uri type을 사용해 해당 주소를 HTTP, FTP 등의 scheme, port 번호, host 등 각각의 요소로 분해하는 문을 아래와 같이 추가합니다.
Console.Write("Enter a valid web address (or press Enter): ");
string? url = Console.ReadLine();
if (string.IsNullOrWhiteSpace(url))
{
url = "http://cliel.com/";
}
Uri uri = new(url);
Console.WriteLine($"URL: {url}");
Console.WriteLine($"Scheme: {uri.Scheme}");
Console.WriteLine($"Port: {uri.Port}");
Console.WriteLine($"Host: {uri.Host}");
Console.WriteLine($"Path: {uri.AbsolutePath}");
Console.WriteLine($"Query: {uri.Query}");
예제를 실행하고 정확한 website주소를 입력하게 되면 아래와 같은 결과를 확인할 수 있습니다.
이어서 입력된 website주소에 대한 ip를 확인할 수 있는 문을 추가합니다.
IPHostEntry entry = Dns.GetHostEntry(uri.Host);
Console.WriteLine($"{entry.HostName} has the following IP addresses:");
foreach (IPAddress address in entry.AddressList)
{
Console.WriteLine($" {address} ({address.AddressFamily})");
}
예제를 실행하고 정확한 website주소를 입력하면 다음의 결과를 표시할 것입니다.
(2) Server로 ping 보내기
위 예제에서 입력된 website주소의 web server 상태는 ping응답을 받음으로써 확인할 수 있습니다. Program.cs에서 이를 구현하기 위한 code를 아래와 같이 추가합니다.
Console.WriteLine($"Host: {uri.Host}");
Ping ping = new();
Console.WriteLine("Pinging server. Please wait...");
PingReply reply = ping.Send(uri.Host);
Console.WriteLine($"{uri.Host} was pinged and replied: {reply.Status}.");
if (reply.Status == IPStatus.Success)
{
Console.WriteLine("Reply from {0} took {1:N0}ms", arg0: reply.Address, arg1: reply.RoundtripTime);
}
예제를 실행하고 정확한 website주소를 입력해 보면 다음의 결과를 표시할 것입니다.