표준 라이브러리: 문자열

표준 라이브러리: 문자열 (Standard library: Strings)

이전 장들에서 String 타입을 사용하는 소스 코드 예시를 봤어요. String은 고정 길이 문자열 타입으로, 기본적으로 문자의 배열이에요. 많은 경우 이 데이터 타입은 텍스트 정보를 다루기에 충분해요. 하지만 더 고급 텍스트 처리가 필요한 상황도 있어요. Ada는 그런 경우를 위한 대안적 접근 방식을 제공해요:

  • 바운드 문자열(Bounded strings): 고정 길이 문자열과 유사하게, 바운드 문자열은 인스턴스화 시 설정되는 최대 길이를 가져요. 하지만 바운드 문자열은 문자의 배열이 아니에요. 언제든 다양한 길이의 문자열을 담을 수 있어요 — 길이가 최대 길이 아래 또는 같기만 하면 되죠.
  • 언바운드 문자열(Unbounded strings): 바운드 문자열과 유사하게, 언바운드 문자열은 다양한 길이의 문자열을 담을 수 있어요. 하지만 그에 더해, 문자열 선언 시 최대 길이를 지정할 필요가 없어요. 이런 의미에서 매우 유연해요.

더 읽어보기 (For further reading...) 언바운드 문자열에 최대 길이를 지정하지는 않지만, 그 한계는 Reference Manual에 정의돼 있어요: Unbounded_String 타입의 객체는 하한이 1이고 길이가 개념적으로 0에서 Natural'Last 사이에서 변할 수 있는 String을 나타내요.

따라서 암묵적 최대 길이는 Natural'Last예요. 반면 바운드 문자열은 Generic_Bounded_Length 패키지가 인스턴스화될 때 지정되는 명시적 최대 길이를 가져요(뒤에서 보게 될 거예요).

바운드 문자열과 언바운드 문자열의 또 다른 차이는 컴파일러가 그 문자열들을 위해 메모리를 할당하는 전략이에요. GNAT를 사용할 때 바운드 문자열은 스택에 할당되고, 언바운드 문자열은 힙에 할당돼요.

다음 절들에서 서로 다른 문자열 타입과 문자열 타입에 대한 일반적인 연산의 개요를 제시할게요.

출처: 표준 라이브러리: 문자열 문서

본문

문자열 연산 (String operations)

표준(고정 길이) 문자열에 대한 연산은 Ada.Strings.Fixed 패키지에 있어요. 앞서 언급했듯 표준 문자열은 고정 길이Character 타입 요소들의 배열이에요. 그래서 이 자식 패키지가 Fixed라고 불려요.

제공되는 가장 간단한 연산 중 하나는 문자열에서 사용 가능한 부분 문자열(substring)의 수를 세는 것(Count)과 그 해당 인덱스를 찾는 것(Index)이에요. 예시를 볼게요:

with Ada.Strings.Fixed; use Ada.Strings.Fixed;
with Ada.Text_IO;       use Ada.Text_IO;

procedure Show_Find_Substring is

   S   : String := "Hello" & 3 * " World";
   P   : constant String := "World";
   Idx : Natural;
   Cnt : Natural;
begin
   Cnt := Ada.Strings.Fixed.Count
     (Source  => S,
      Pattern => P);

   Put_Line ("String: " & S);
   Put_Line ("Count for '" & P & "': "
             & Natural'Image (Cnt));

   Idx := 0;
   for I in 1 .. Cnt loop
      Idx := Index
        (Source  => S,
         Pattern => P,
         From    => Idx + 1);

      Put_Line ("Found instance of '"
                & P & "' at position: "
                & Natural'Image (Idx));
   end loop;

end Show_Find_Substring;

곱셈을 사용해 문자열 S를 초기화해요. "Hello" & 3 * " World"를 쓰면 Hello World World World 문자열을 만들어요. 그런 다음 Count 함수를 호출해 S에서 단어 World의 인스턴스 수를 얻어요. 다음으로 루프에서 Index 함수를 호출해 S에서 World의 각 인스턴스의 인덱스를 찾아요.

그 예시는 특정 부분 문자열의 인스턴스를 찾았어요. 다음 예시에서는 문자열의 모든 단어를 검색해요. Find_Token을 사용하고 공백(whitespace)을 구분자로 지정해 이걸 해요. 예를 들어:

with Ada.Strings;       use Ada.Strings;
with Ada.Strings.Fixed; use Ada.Strings.Fixed;
with Ada.Strings.Maps;  use Ada.Strings.Maps;
with Ada.Text_IO;       use Ada.Text_IO;

procedure Show_Find_Words is

   S   : String := "Hello" & 3 * " World";
   F   : Positive;
   L   : Natural;
   I   : Natural := 1;

   Whitespace : constant Character_Set :=
     To_Set (' ');
begin
   Put_Line ("String: " & S);
   Put_Line ("String length: "
             & Integer'Image (S'Length));

   while I in S'Range loop
      Find_Token
        (Source  => S,
         Set     => Whitespace,
         From    => I,
         Test    => Outside,
         First   => F,
         Last    => L);

      exit when L = 0;

      Put_Line ("Found word instance at position "
                & Natural'Image (F)
                & ": '" & S (F .. L) & "'");
      --   & "-" & F'Img & "-" & L'Img

      I := L + 1;
   end loop;
end Show_Find_Words;

구분자로 사용될 문자 집합을 Find_Token 프로시저에 전달해요. 이 집합은 Ada.Strings.Maps 패키지의 Character_Set 타입의 멤버예요. (같은 패키지의) To_Set 함수를 호출해 집합을 Whitespace로 초기화하고, 그런 다음 각 유효한 인덱스를 순회하며 각 단어의 시작 인덱스를 찾도록 Find_Token을 호출해요. Find_Token 프로시저의 Test 매개변수에 Outside를 전달해 Whitespace 집합 밖의 인덱스, 즉 실제 단어들을 찾고 있음을 나타내요. Find_TokenFirstLast 매개변수는 부분 문자열의 유효한 범위를 나타내는 출력 매개변수예요. 이 정보를 사용해 문자열(S (F .. L))을 표시해요.

지금까지 본 연산들은 문자열을 읽지만 수정하지는 않아요. 이제 문자열의 내용을 변경하는 연산을 논의할게요:

연산 설명
Insert 문자열에 부분 문자열 삽입
Overwrite 부분 문자열로 문자열 덮어쓰기
Delete 부분 문자열 삭제
Trim 문자열에서 공백 제거

이 모든 연산은 함수와 프로시저로 둘 다 사용할 수 있어요. 함수는 새 문자열을 만들지만, 프로시저는 제자리에서 연산을 수행해요. 프로시저는 문자열의 제약이 충족되지 않으면 예외를 발생시켜요. 예를 들어 10개의 문자를 포함하는 문자열 S가 있고, 두 문자(예: "!!")가 있는 문자열을 여기에 삽입하면 12개의 문자를 포함하는 문자열이 생성돼요. 고정 길이이므로 크기를 늘릴 수 없어요. 이 경우 가능한 해결책 중 하나는 부분 문자열을 삽입하는 동안 절단(truncation)이 적용되도록 지정하는 것이에요. 이렇게 하면 S의 길이가 고정으로 유지돼요. Insert, Overwrite, Delete의 함수 버전과 프로시저 버전을 모두 사용하는 예시를 볼게요:

with Ada.Strings;       use Ada.Strings;
with Ada.Strings.Fixed; use Ada.Strings.Fixed;
with Ada.Text_IO;       use Ada.Text_IO;

procedure Show_Adapted_Strings is

   S   : String := "Hello World";
   P   : constant String := "World";
   N   : constant String := "Beautiful";

   procedure Display_Adapted_String
     (Source   : String;
      Before   : Positive;
      New_Item : String;
      Pattern  : String)
   is
      S_Ins_In : String := Source;
      S_Ovr_In : String := Source;
      S_Del_In : String := Source;

      S_Ins : String :=
        Insert (Source,
                Before,
                New_Item & " ");
      S_Ovr : String :=
        Overwrite (Source,
                   Before,
                   New_Item);
      S_Del : String :=
        Trim (Delete (Source,
                      Before,
                      Before +
                        Pattern'Length - 1),
              Ada.Strings.Right);
   begin
      Insert (S_Ins_In,
              Before,
              New_Item,
              Right);

      Overwrite (S_Ovr_In,
                 Before,
                 New_Item,
                 Right);

      Delete (S_Del_In,
              Before,
              Before + Pattern'Length - 1);

      Put_Line ("Original:  '"
                & Source & "'");

      Put_Line ("Insert:    '"
                & S_Ins  & "'");
      Put_Line ("Overwrite: '"
                & S_Ovr  & "'");
      Put_Line ("Delete:    '"
                & S_Del  & "'");

      Put_Line ("Insert    (in-place): '"
                & S_Ins_In & "'");
      Put_Line ("Overwrite (in-place): '"
                & S_Ovr_In & "'");
      Put_Line ("Delete    (in-place): '"
                & S_Del_In & "'");
   end Display_Adapted_String;

   Idx : Natural;
begin
   Idx := Index
     (Source  => S,
      Pattern => P);

   if Idx > 0 then
      Display_Adapted_String (S, Idx, N, P);
   end if;
end Show_Adapted_Strings;

이 예시에서 부분 문자열 World의 인덱스를 찾고 바깥 문자열 안의 이 부분 문자열에 연산을 수행해요. 프로시저 Display_Adapted_String은 두 버전의 연산을 모두 사용해요. InsertOverwrite의 프로시저 버전에서는 문자열의 오른쪽(Right)에 절단을 적용해요. Delete 프로시저에서는 부분 문자열의 범위를 지정하는데, 이는 공백으로 대체돼요. Delete의 함수 버전에서는 후행 공백을 잘라내는 Trim도 호출해요.

고정 길이 문자열의 한계 (Limitation of fixed-length strings)

고정 길이 문자열을 사용하는 것은 선언할 때 초기화되는 문자열에는 보통 충분해요. 하지만 앞 절에서 봤듯이 문자열에 대한 프로시저 연산은 고정 길이 문자열에서 하면 어려움이 생겨요. 고정 길이 문자열이 문자의 배열이기 때문이에요. 다음 예시는 선언에서 초기화되지 않을 때 고정 길이 문자열의 초기화가 얼마나 번거로운지 보여줘요:

with Ada.Text_IO;         use Ada.Text_IO;

procedure Show_Char_Array is
   S : String (1 .. 15);
   --  문자열은 Character의 배열
begin
   S := "Hello          ";
   --  대안:
   --
   --  #1:
   --      S (1 .. 5)      := "Hello";
   --      S (6 .. S'Last) := (others => ' ');
   --
   --  #2:
   --      S := ('H', 'e', 'l', 'l', 'o',
   --            others => ' ');

   Put_Line ("String: " & S);
   Put_Line ("String Length: "
             & Integer'Image (S'Length));
end Show_Char_Array;

이 경우 단순히 S := "Hello"라고 쓸 수 없어요. Hello 상수에 대한 결과 문자의 배열이 S 문자열과 길이가 다르기 때문이에요. 따라서 S의 길이와 일치하도록 후행 공백을 포함해야 해요. 예시에 나와 있듯 초기화에 정확한 범위(S (1 .. 5))를 사용하거나 개별 문자의 명시적 배열을 사용할 수 있어요.

문자열이 런타임에 초기화되거나 조작될 때는 보통 바운드 또는 언바운드 문자열을 사용하는 것이 더 좋아요. 이 타입들의 중요한 특징은 배열이 아니라는 것이에요. 그래서 위에 제시된 어려움이 적용되지 않아요. 바운드 문자열부터 시작할게요.

바운드 문자열 (Bounded strings)

바운드 문자열은 Ada.Strings.Bounded.Generic_Bounded_Length 패키지에 정의돼 있어요. 이 패키지는 제네릭이므로 인스턴스화하고 바운드 문자열의 최대 길이를 설정해야 해요. 그런 다음 Bounded_String 타입의 바운드 문자열을 선언할 수 있어요.

바운드 문자열과 고정 길이 문자열은 둘 다 담을 수 있는 최대 길이를 가져요. 하지만 바운드 문자열은 배열이 아니므로 런타임에 초기화하는 것이 훨씬 쉬워요. 예를 들어:

with Ada.Strings;         use Ada.Strings;
with Ada.Strings.Bounded;
with Ada.Text_IO;         use Ada.Text_IO;

procedure Show_Bounded_String is
   package B_Str is new
     Ada.Strings.Bounded.Generic_Bounded_Length
       (Max => 15);
   use B_Str;

   S1, S2 : Bounded_String;

   procedure Display_String_Info
     (S : Bounded_String)
   is
   begin
      Put_Line ("String: " & To_String (S));
      Put_Line ("String Length: "
                & Integer'Image (Length (S)));
      --  String:
      --          S'Length => ok
      --  Bounded_String:
      --          S'Length => 컴파일 오류:
      --                      바운드 문자열은
      --                      배열이 아님!

      Put_Line ("Max.   Length: "
                & Integer'Image (Max_Length));
   end Display_String_Info;

begin
   S1 := To_Bounded_String ("Hello");
   Display_String_Info (S1);

   S2 := To_Bounded_String ("Hello World");
   Display_String_Info (S2);

   S1 := To_Bounded_String
     ("Something longer to say here...",
      Right);
   Display_String_Info (S1);
end Show_Bounded_String;

바운드 문자열을 사용하면 실행 중에 S1S2에 여러 번 쉽게 할당할 수 있어요. To_Bounded_StringTo_String 함수를 사용해 고정 길이 문자열과 바운드 문자열 사이에서 각각 해당 방향으로 변환해요. 입력 문자열의 길이가 바운드 문자열의 최대 용량보다 크면 To_Bounded_String 호출이 예외를 발생시켜요. 이를 피하려면 절단 매개변수(예시의 Right)를 사용할 수 있어요.

바운드 문자열은 배열이 아니므로 고정 길이 문자열에서 했던 것처럼 'Length 애트리뷰트를 사용할 수 없어요. 대신 바운드 문자열의 길이를 반환하는 Length 함수를 호출해요. Max_Length 상수는 패키지를 인스턴스화할 때 설정한 바운드 문자열의 최대 길이를 나타내요.

바운드 문자열을 초기화한 후 조작할 수 있어요. 예를 들어 Append로 바운드 문자열에 문자열을 추가하거나 & 연산자로 바운드 문자열을 연결할 수 있어요. 다음과 같이요:

with Ada.Strings;         use Ada.Strings;
with Ada.Strings.Bounded;
with Ada.Text_IO;         use Ada.Text_IO;

procedure Show_Bounded_String_Op is
   package B_Str is new
     Ada.Strings.Bounded.Generic_Bounded_Length
       (Max => 30);
   use B_Str;

   S1, S2 : Bounded_String;
begin
   S1 := To_Bounded_String ("Hello");
   --  대안:
   --
   --  A := Null_Bounded_String & "Hello";

   Append (S1, " World");
   --  대안:
   --    Append (A, " World", Right);

   Put_Line ("String: " & To_String (S1));

   S2 := To_Bounded_String ("Hello!");
   S1 := S1 & " " & S2;
   Put_Line ("String: " & To_String (S1));
end Show_Bounded_String_Op;

Null_Bounded_String 상수를 사용해 바운드 문자열을 빈 문자열로 초기화할 수 있어요. 또한 Append 프로시저를 사용하고 To_Bounded_String 함수에서처럼 절단 모드를 지정할 수 있어요.

언바운드 문자열 (Unbounded strings)

언바운드 문자열은 Ada.Strings.Unbounded 패키지에 정의돼 있어요. 이 패키지는 제네릭이 아니므로 Unbounded_String 타입을 사용하기 전에 인스턴스화할 필요가 없어요. 이전 절에서 기억하겠지만 바운드 문자열은 패키지 인스턴스화가 필요해요.

언바운드 문자열은 바운드 문자열과 유사해요. 주요 차이점은 어떤 크기의 문자열도 담을 수 있고 입력 문자열에 따라 조정된다는 것이에요: 예를 들어 10자 문자열을 언바운드 문자열에 할당하고 나중에 50자 문자열을 할당하면, 컨테이너의 내부 연산이 새 문자열을 저장할 메모리를 할당하도록 보장해요. 대부분의 경우 개발자는 이 연산들에 대해 걱정할 필요가 없어요. 또한 절단도 필요 없어요.

언바운드 문자열의 초기화는 바운드 문자열과 매우 유사해요. 예시를 볼게요:

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Strings; use Ada.Strings;

with Ada.Strings.Unbounded;
use  Ada.Strings.Unbounded;

procedure Show_Unbounded_String is
   S1, S2 : Unbounded_String;

   procedure Display_String_Info
     (S : Unbounded_String)
   is
   begin
      Put_Line ("String: " & To_String (S));
      Put_Line ("String Length: "
                & Integer'Image (Length (S)));
   end Display_String_Info;
begin
   S1 := To_Unbounded_String ("Hello");
   --  대안:
   --
   --  A := Null_Unbounded_String & "Hello";

   Display_String_Info (S1);

   S2 := To_Unbounded_String ("Hello World");
   Display_String_Info (S2);

   S1 := To_Unbounded_String
           ("Something longer to say here...");
   Display_String_Info (S1);
end Show_Unbounded_String;

바운드 문자열처럼 실행 중에 S1S2에 여러 번 할당할 수 있고 To_Unbounded_StringTo_String 함수를 사용해 고정 길이 문자열과 언바운드 문자열 사이에서 왕복 변환을 할 수 있어요. 하지만 이 경우 절단은 필요 없어요.

그리고 바운드 문자열과 마찬가지로 언바운드 문자열에도 Append 프로시저와 & 연산자를 사용할 수 있어요. 예를 들어:

with Ada.Text_IO; use Ada.Text_IO;

with Ada.Strings.Unbounded;
use  Ada.Strings.Unbounded;

procedure Show_Unbounded_String_Op is
   S1, S2 : Unbounded_String :=
              Null_Unbounded_String;
begin
   S1 := S1 & "Hello";
   S2 := S2 & "Hello!";

   Append (S1, " World");
   Put_Line ("String: " & To_String (S1));

   S1 := S1 & " " & S2;
   Put_Line ("String: " & To_String (S1));
end Show_Unbounded_String_Op;

이 예시에서 언바운드 S1S2 문자열을 각각 "Hello""Hello!" 문자열과 연결해요. 또한 바운드 문자열에서 했던 것처럼 Append 프로시저를 사용해요.

더 알아보기 (Learn more)