본문 바로가기

C#

2023.09.13 BCL (시간, 문자열, StringBuilder, 정규 표현식, 컬렉션, ArrayList, Hashtable)

BCL (Base Class Library)

C#과 같은 언어로 만들어진 프로그램에서 운영체제와 연동할 수 있게 관련 기능을 모아서 담아 놨다.

운영체제의 소켓, 스레드, 파일, 레지스트리 등에 접근하고 싶다면 BCL에서 제공하는 클래스를 사용하면 된다.

 

BCL은 운영체제와 중계 역할만 하는 것은 아니다. 처리에 해당하는 과정에서 자주 사용되는 것들도 포함된다.

예를 들어, 데이터 처리 과정 중에 다양한 수학적인 연산을 포함시키는 경우가 있다. Log, Cos 등의 메서드들은 이미 Math 타입으로 제공된다.

닷넷의 버전이 올라가며 BCL에도 기능들이 추가되는데 최신 기능을 이전 버전에서는 사용할 수 없다.

 

시간

System.DateTime

DateTime은 struct로 정의된 값 형식이다. 

속성 중에서는 Now, Year, Month, Day, Hour, Minute, Second, Millisecond가 자주 사용된다.

DateTime now = DateTime.Now;
Console.WriteLine(now); // 2023-09-13 오후 1:13:42

DateTime dayForChildren = new DateTime(now.Year, 5, 5);
Console.WriteLine(dayForChildren); // 2023-05-05 오전 12:00:00

밀리초보다 정밀도가 더 높은 시간값이 필요하면 Ticks속성을 이용하면 된다. 이 값은 1년 1월 1일 12시 자정을 기준으로 현재까지 100 나노 초 간격으로 흐른 숫자값이다. 다음은 메서드가 실행되는 데 걸린 시간을 Ticks를 이용해 계산하는 예제이다.

using System;

class Program
{
    static void Main(string[] args)
    {
        DateTime before = DateTime.Now;
        Sum();
        DateTime after = DateTime.Now;

        long gap = after.Ticks - before.Ticks;

        Console.WriteLine("Total Ticks: " + gap);
        Console.WriteLine("Millisecond: " + (gap / 10000));
        Console.WriteLine("Second: " + (gap / 10000 / 1000));
    }

    private static long Sum()
    {
        long sum = 0;
        for (int i = 0; i < 1000000; i++)
        {
            sum += i;
        }
        return sum;
    }
}

----------------출력 결과--------------------
Total Ticks: 29991
Millisecond: 2
Second: 0

시간대가 반영된 것을 지역 시간(local time)이라 한다.

영국을 제외하고 거의 모든 나라에서 UTC인지 지역 시간인지 명시해야만 정확한 시간을 알 수 있다.

닷넷의 DateTime 타입은 이 구분을 열거형 타입인 Kind 속성으로 알려준다.

 

닷넷에서 시간의 기준값은 1년 1월 1일이지만, 유닉스 및 자바 관련 플랫폼에서는 1970년 1월 1일을 기준으로 한다.

따라서 자바/자바스크립트 등에서 구한 밀리초 값을 닷넷에서 구한 값과 정상적으로 비교하려면 1970년에 해당하는 고정 밀리초 값을 빼야 한다.

long javaMillis = (DateTime.UtcNow.Ticks - 621355968000000000) / 10000;

System.TimeSpan

DateTime 타입에 대해 사칙 연산 중에서 유일하게 허용되는 것이 빼기이다. 그리고 빼기의 연산 결괏값은 2개의 

DateTime 사이의 시간 간격을 나타내는 TimeSpan으로 나온다.

DateTime endOfYear = new DateTime(DateTime.Now.Year, 12, 31);
DateTime now = DateTime.Now;

TimeSpan gap = endOfYear - now;

Console.WriteLine(gap.TotalDays);

System.Diagnosics.Stopwatch

TimeSpan보다 더 정확한 시간차 계산을 위해  Stopwatch 타입을 제공한다. 

보통 코드의 특정 구간에 대한 성능을 측정할 때 자주 사용된다.

 

문자열 처리

System.String

Split - 주어진 문자 또는 문자열을 구분자로 나뉜 문자열의 배열로 반환

Substring - 시작과 길이에 해당하는 만큼의 문자열을 반환

ToLower - 문자열을 소문자로 변환해서 반환

ToUpper - 문자열을 대문자로 변환해서 반환

Length - 문자열의 길이를 정수로 반환

 

대소문자 구분의 오버로드 버전을 제공하는 메서드로 EndWith, IndexOf, StartsWith가 있다 이 메서드들은 각각 

StringComparison 열거형 인자를 추가로 받을 수 있다. 이 인자를 생략하면 기본적으로 대소문자 구분을 하고,

대소문자 구분을 하고 싶지 않다면 StringComparison.OrdinalIgnoreCase 인자를 함께 전달하면 된다.

문자열의 == 연산자는 대소문자를 무시하는 기능은 없지만 대신 Equals 메서드로 바꾸면 가능하다.

 

마지막으로 Format 메서드는 인자를 형식 문자열에 포함된 번호와 맞춰서 치환하는 기능이다.

string txt = "Hello {0]: {1}";

string output = string.Format(txt, "world", "Anderson");
Console.WriteLine(output);

//출력 결과
Hello World: Anderson

string, Format의 첫 번째 인자에는 중괄호로 둘러싸인 번호를 포함할 수 있다.

뒤이어 오는 인자의 위치와 대응되어 치환된다.

또한 형식 문자열의 번호는 중복 사용이 가능하고 순서에도 제약이 없다.

번호와 대응되는 인자가 반드시 string 형식일 필요는 없다. string 형식이 아닌 타입의 인스턴스가

인자로 대응되면 그것의 ToString 메서드를 호출한 결과를 출력한다.

 

System.Text.StringBuilder

string 타입은 불변 객체이기 때문에 string에 대한 모든 변환은 새로운 메모 할당을 발생시킨다.

예를 들어 string.ToLower 메서드를 보자.

string txt = "Hello World";
string lwrText = txt.ToLower();

txt 변수는 힙에 있는 "Hello World"를 가리킨다. 그 상태에서 ToLower 메서드를 호출하면 txt 변수에 담긴

문자열이 소문자로 변경되는 것이 아니라 원문이 통째로 복사된 다음 그것이 소문자로 변경되어 반환되는 과정을 거친다.

string 클래스가 발생시키는 가장 큰 문제는 문자열을 더할 때다.

static void Main(string[] args)
        {            
            string txt = "Hello World";

            Stopwatch sw = new Stopwatch();

            sw.Start();
            for (int i = 0; i < 300000; i++)
            {
                txt = txt + "1";
            }
            sw.Stop();

            Console.WriteLine(sw.ElapsedMilliseconds / 1000);
        }

약 10초가 걸렸다. 오래걸린 이유는 다음과 같다.

txt + "1" 동장을 수행하기 위해 txt.Length + "1".Length에 해당하는 크기의 메모리를 힙에 할당한다.

그 메모리에 txt 변수가 가리키는 힙의 문자열과 "1" 문자열을 복사한다. 

이 과정을 30만번 반복한다.

이런 문제를 해결하기 위해 BCL에 추가된 클래스가 StringBuilder이다. Append 메서드를 제공하는데 

위 예제를 StringBuilder를 이용해 개선하면 다음과 같다.

static void Main(string[] args)
        {    
            string txt = "Hello World";

            StringBuilder sb = new StringBuilder();
            sb.Append(txt);
            Stopwatch sw = new Stopwatch();

            sw.Start();
            for (int i = 0; i < 300000; i++)
            {
               sb.Append("1");
            }

            string newTxt = sb.ToString();
            sw.Stop();

            Console.WriteLine(sw.ElapsedMilliseconds);
        }

0.001초가 걸렸다. 이번에도 내부 연산 과정을 통해 그 이유를 알아보자

  1. StringBuilder는 내부적으로 일정한 양의 메모리를 미리 할당한다.
  2. Append 메서드에 들어온 인자를 미리 할당한 메모리에 복사한다.
  3. 2번 과정을 30만번 반복한다. Append로 추가된 문자열이 미리 할당한 메모리보다 많아지면 새롭게 여유분의 
    메모리를 할당한다.
  4. ToString 메서드를 호출하면 연속적으로 연결된 하나의 문자열을 반환한다.

즉, 잦은 메모리 할당과 복사가 없어졌기 때문에 그만큼 성능이 향상된 것이다. 이 때문에 문자열을 연결하는 직업이 많을 때는 반드시 StringBuilder를 사용하는 것을 권장한다.

 

System.Text.RegularExpressions.Regex

정규 표현식은 문자열 처리에 대한 일반적인 규칙을 표현하는 형식 언어다.

사용자가 이메일 형식에 맞게 입력했는지 판별하는 예제

static void Main(string[] args)
        {
            string email = "tester@test.com";
            Console.WriteLine(IsEmail2(email));
        }

        static bool IsEmail2(string email)
        {
            Regex regex =
                 new Regex(@"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z]+$");
            return regex.IsMatch(email);
        }

Regex 타입에는 패턴 일치를 판단하는 IsMatch 메서드뿐 아니라 패턴과 일치하는 문장을 다른 문장으로 치환하는 

Replace 메서드도 제공된다.

Replace 기능은 string 타입에서도 제공되지만 아쉽게도 대소문자가 구분되어 동작한다. 즉, 

string.Replace("World", "Universe"); 코드는 대소문자가 다른 "World" 단어는 치환하지 못한다.

이런 경우 정규 표현식을 이용하는 편이 더 쉽다.

static void Main(string[] args)
        {
            string txt = "Hello World! Welcome to my world";

            Regex regex = new Regex("world", RegexOptions.IgnoreCase); // 대소문자 구분 x
            string result = regex.Replace(txt, funcMatch);

            Console.WriteLine(result);

        }

        static string funcMatch(Match match)
        {
            return "Universe";
        }

직렬화 / 역직렬화

System.BitConverter

BitConverter로 변환된 바이트 배열은 그 순서가 거꾸로 되어 있다. 그 이유는 리틀 엔디안과 빅 엔디안의 바이트 순서 차이 때문이다. 거꾸로 표현하는 방식을 리틀 엔디안이라 하고, 차례대로 표현하는 방식을 빅 엔디안이라 한다.

인텔 호환 CPU에서는 모두 리틀 엔디안을 사용한다. 반면 RISC 프로세서 계열에서는 빅 엔디안을 사용한다.

따라서 2바이트 이상으로 표현되는 short, ushort, int, uint, long, ulong, float, double에서는 엔디안 정렬에 유의한다.

System.IO.MemoryStream

  • 메모리 내에서 스트림을 생성합니다.
  • 크기가 가변적입니다.
  • 데이터를 효율적으로 처리할 수 있습니다.

System.IO.StreamWriter와 System.IO.StreamReader

  • 텍스트를 스트림에 읽고 씁니다.
  • 다양한 인코딩을 지원합니다.
  • 파일, 네트워크 연결, 메모리 스트림 등 다양한 스트림에 사용할 수 있습니다.

System.IO.BinaryWriter와 System.IO.BinaryReader

  • 이진 데이터를 스트림에 읽고 씁니다.
  • 파일, 네트워크 연결, 메모리 스트림 등 다양한 스트림에 사용할 수 있습니다.

컬렉션

지금까지 배열은 크기가 고정돼 있다는 특징이 있다. 물론, 변수 자체에 대해서는 재할당을 통해 크기를 바꾸는 것이 가능하지만 이전의 데이터가 보존되지 않는다.

정해지지 않은 크기의 배열을 다룰 때 이런 기능을 편리하게 구현한 것을 컬렉션이라 한다.

BCL에서는 System.Collections 네임스페이스 하위에 이와 관련된 타입을 묶어서 제공하고 있다.

System.Collections.ArrayList

object 타입 및 그와 형변환할 수 있는 모든 타입을 인자로 받아 컬렉션에 추가/삭제/변경/조회할 수 있는 기능을 구현한

타입이다. 

            ArrayList ar = new ArrayList();

            ar.Add("Hello");
            ar.Add(6);
            ar.Add("World");
            ar.Add(true);

            Console.WriteLine("Contains(6): " + ar.Contains(6));

            ar.Remove("World");

            ar[2] = false;

            Console.WriteLine();

            foreach (object obj in ar)
            {
                Console.WriteLine(obj);
            }
            
            ---------출력 결과----------
            Contains(6): True
            
            Hello
            6
            False

ArrayList는 object를 인자로 갖기 때문에 닷넷의 모든 타입을 담을 수 있다는 장점이 있지만 반대로 이로 인해 

박싱(값 형식을 object형태의 참조 형식으로 변환)이 발생한다는 단점이 있다.

따라서 System.ValueType을 상속받는 값 형식을 위한 컬렉션으로는 적당하지 않다.

닷넷 2.0부터 지원되는 제네릭이 적용된 List <T> 타입을 사용하는 것이 권장된다.

 

ArrayList는 요소를 정렬할 수 있는 메서드도 제공한다. 배열의 경우에는 Array.Sort 정적 메서드를 이용했지만

ArrayList에는 인스턴스 메서드로 Sort가 제공된다. 제약 사항으로는 요소가 모두 같은 타입이어야 한다는 것이다.

다른 타입이 섞여 있으면 ArgumentException 예외가 발생한다.

ArrayList ar = new ArrayList();

ar.Add("Hello");
ar.Add("World");
ar.Add("My");
ar.Add("Sample");

ar.Sort();

foreach (string txt in ar)
{
    Console.WriteLine(txt);
}


----------------출력 결과----------------
Hello
My
Sample
World

사용자 정의 타입을 요소로 가지고 있다면 어떻게 Sort 할까?

이번에는 IComparable이라는 또 다른 인터페이스를 이용한 방법을 설명하겠다.

ArrayList.Sort 메서드는 기본적으로 요소의 객체가 IComparable 인터페이스를 구현하고 있는지 확인한다.

만약 그렇다면 해당 메서드의 CompareTo 메서드를 호출해 그 결과로 정렬 작업을 수행한다. 아래 예제에서는

Person 타입이 스스로 IComparable 인터페이스를 구현하고 있다. 따라서 ArrayList에서 단순히 Sort를 호출하기만 해도

요소가 정렬된다.

 public class Person : IComparable
    {
        public int Age;
        public string Name;

        public Person(int age, string name)
        {
            this.Age = age;
            this.Name = name;
        }

        public int CompareTo(object obj)
        {
            Person target = (Person)obj;

            if (this.Age > target.Age) return 1;
            else if (this.Age == target.Age) return 0;

            return -1;
        }

        public override string ToString()
        {
            return string.Format("{0}({1})", this.Name, this.Age);
        }
    }

   class Program
   {
        static void Main(string[] args)
        {
            ArrayList ar = new ArrayList();

            ar.Add(new Person(32, "Cooper"));
            ar.Add(new Person(56, "Anderson"));
            ar.Add(new Person(17, "Sammy"));
            ar.Add(new Person(27, "Paul"));

            ar.Sort();

            foreach(Person person in ar)
            {
                Console.WriteLine(person);
            }

        }
    }
    
    -----------------출력 결과----------------
    Sammy(17)
    Paul(27)
    Cooper(32)
    Anderson(56)

 

System.Collections.Hashtable

ArrayList와 함께, 자주 사용되는 컬렉션으로 Hashtable이 있다. 이 컬렉션은 값뿐만 아니라 해시에서

사용되는 키가 추가되어 빠른 검색 속도를 자랑한다. 따라서 검색 속도의 중요도에 따라 ArrayList 또는

Hashtable을 선택할지 결정한다.

 

Hashtable과 ArrayList의 검색 속도 비교를 위해 공통으로 제공되는 Remove 메서들를 예로 들어보자

우선 ArrayList.Remove는 다음과 같은 방식으로 동작한다.

  1. 0번째 요소의 값과 Remove 인자의 값을 비교한다. 같으면 삭제하고 return문을 수행한다.
  2. 1번째 단계에서 값을 찾지 못하면 그다음 요소의 값과 비교한다. 값이 같으면 삭제하고 return문을 수행한다.
  3. 값을 찾을 때까지 반복한다. 값이 존재하지 않는 경우 ArrayList 전체 요소의 값을 열람할 수밖에 없다.

Hashtable의 경우를 보자

  1. Remove 메인자로 들어온 Key 값을 해시한다. 예를 들어, "key1"문자열이 키 값인 경우  "key1".GetHashCode()
    메서드가 호출된다.
  2. GetHashCode 메서드 호출의 결괏값은 정수이다. 그 정수는 내부 데이터 저장소에 대해 곧바로 접근할 수 있는
    인덱스로 사용된다. 따라서 값을 검색하는 과정 없이 곧바로 저장된 값의 위치를 알 수 있다.
  3. 해시에 해당하는 위치에 값이 있다면 삭제하고, 없다면 더는 추가적인 동작을 수행하지 않고 메서드 실해을 마친다.

이런 검색 과정의 차이를 빅-오 표기법으로 나타내면 ArrayList는 O(N)이고, Hashtable은 O(1)이 된다.

크기가 작은 컬렉션인 경우 O(N)의 성능이 더 좋기 때문에 ArrayList를 써도 무방하다.

            Hashtable ht = new Hashtable();

            ht.Add("key1", "add");
            ht.Add("key2", "remove");
            ht.Add("key3", "update");
            ht.Add("key4", "search");

            Console.WriteLine(ht["key4"]);

            ht.Remove("key3");

            ht["key2"] = "delete";

            Console.WriteLine();

            // 모든 키 값을 열람하고 그 키에 대응하는 값을 출력
            foreach (object key in ht.Keys)
            {
                Console.WriteLine("{0} ==> {1}",key, ht[key]);
            }
------------출력 결과------------
search

key4 ==> search
key1 ==> add
key2 ==> delete

Hashtable 사용 시 주의 사항으로 키 값이 중복되지 않아야 한다. 중복되는 경우 Add 메서드에서 

ArgumentException 예외가 발생하므로 중복 키에 주의를 기울여야 한다.

또한 Hashtable은 ArrayList와는 달리 키 값도 내부적으로 보관하고 있기 때문에 그만큼 메모리 낭비가 있다.

게다가 키와 값 모두 object 타입으로 다뤄지기 때문에 Hashtable에서도 박싱 문제가 발생한다.

 

System.Collections.Stack

System.Collections.Queue

둘 다 임시로 객체를 저장하는 데 사용되는 자료구조입니다. 하지만, 내부 자료구조와 알고리즘이 다르고, 사용 목적도 

다르다.

  • System.Collections.Stack은 FILO(First In Last Out) 자료구조이다. 즉, 가장 마지막에 추가된 객체가 가장 먼저 제거된다. 스택은 종종 되돌리기/다시하기 기능을 구현하거나, 프로그램 실행 순서를 추적하는 데 사용된다.
  • System.Collections.Queue는 FIFO(First In First Out) 자료구조이다. 즉, 가장 먼저 추가된 객체가 가장 먼저 제거됩니다. 큐는 종종 대기 목록을 구현하거나, 작업을 순서대로 처리하는 데 사용된다.

일반적으로 가장 최근에 추가된 항목을 추적해야 할 때는 System.Collections.Stack을 사용하고, 항목을 순서대로 처리해야 할 때는 System.Collections.Queue를 사용한다.

 

두 타입 모두 object를 인자로 다루기 때문에 박싱 문제가 발생한다.

https://www.sysnet.pe.kr/0/0

 

J & J - 정성태의 닷넷 이야기: Digital Stories

 

www.sysnet.pe.kr

https://ridibooks.com/books/1160000100

 

시작하세요! C# 10 프로그래밍

시작하세요! C# 10 프로그래밍 작품소개: 이 책의 목표는 여러분이 C#을 이용해 프로그래밍 기초를 탄탄하게 다질 수 있게 하는 것이다. 이를 위해 C# 언어의 최신 버전인 C# 10의 문법까지 구체적인

ridibooks.com