StringTokenizer cho .NET Core

Sep 18 2020

Tách một chuỗi trong mã thông báo là một chủ đề phức tạp hơn String.Split () muốn khiến chúng ta tin tưởng. Có ít nhất ba chính sách phổ biến mà theo đó, một chuỗi có thể được diễn giải và chia thành các mã thông báo.

Chính sách 1: Tương đương với String.Split ()

Không có nhiều điều để đề cập về chính sách này. Cho một chuỗi svà một dấu phân cách d, ngắt sthành các đoạn được phân cách bằng d. Hạn chế chính ở đây là nếu dấu phân cách là một phần của ít nhất một trong các mã thông báo, việc tạo lại các mã thông báo mong muốn có thể tốn kém.

Chính sách 2: Thoát các ký tự đặc biệt

Một ký tự được khai báo là ký tự thoát e (thường là dấu gạch chéo ngược \) dẫn đến ký tự theo sau nó mất đi ý nghĩa đặc biệt. Một chuỗi mã thông báo sau đó có thể trông như thế này:

token_1 token_2 very\ long \ token

sẽ tương đương với

{ "token_1", "token_2", "very long token" }

Chính sách 3: Đặt mã thông báo trong dấu ngoặc kép

Ví dụ, phương pháp này được sử dụng trong các tệp CSV được tạo trong MSExcel. Mọi thứ giữa dấu ngoặc kép được coi như một mã thông báo. Nếu dấu ngoặc kép "là một phần của mã thông báo, chúng sẽ được nhân đôi "". Một chuỗi mã thông báo sau đó có thể trông như thế này:

token_1,token_2,"token2,5"

sẽ tương đương với

{ "token_1", "token_2", "token2,5" }

Mã

using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;

namespace Pillepalle1.ConsoleTelegramBot.Model.Misc
{
    public sealed class StringTokenizer
    {
        private string _sourceString = null;                            // Provided data to split

        #region Constructors
        /// <summary>
        /// Creates a new StringTokenizer
        /// </summary>
        /// <param name="dataInput">Data to be split into tokens</param>
        public StringTokenizer(string dataInput)
        {
            _sourceString = dataInput ?? string.Empty;
        }
        #endregion

        #region Interface
        /// <summary>
        /// Access tokens by index
        /// </summary>
        public string this[int index]
        {
            get
            {
                if (index >= this.Count)
                {
                    return String.Empty;
                }

                return _Tokens[index];
            }
        }

        /// <summary>
        /// How many tokens does the command consist of
        /// </summary>
        public int Count
        {
            get
            {
                return _Tokens.Count;
            }
        }

        /// <summary>
        /// Which strategy is used to split the string into tokens
        /// </summary>
        public StringTokenizerStrategy Strategy
        {
            get
            {
                return _strategy;
            }
            set
            {
                if (value != _strategy)
                {
                    _strategy = value;
                    _tokens = null;
                }
            }
        }
        private StringTokenizerStrategy _strategy = StringTokenizerStrategy.Split;

        /// <summary>
        /// Character used to delimit tokens
        /// </summary>
        public char Delimiter
        {
            get
            {
                return _delimiter;
            }
            set
            {
                if (value != _delimiter)
                {
                    _delimiter = value;
                    _tokens = null;
                }
            }
        }
        private char _delimiter = ' ';

        /// <summary>
        /// Character used to escape the following character
        /// </summary>
        public char Escape
        {
            get
            {
                return _escape;
            }
            set
            {
                if (value != _escape)
                {
                    _escape = value;

                    if (Strategy == StringTokenizerStrategy.Escaping)
                    {
                        _tokens = null;
                    }
                }
            }
        }
        private char _escape = '\\';

        /// <summary>
        /// Character used to surround tokens
        /// </summary>
        public char Quotes
        {
            get
            {
                return _quotes;
            }
            set
            {
                if (value != _quotes)
                {
                    _quotes = value;

                    if (Strategy == StringTokenizerStrategy.Quotation)
                    {
                        _tokens = null;
                    }
                }
            }
        }
        private char _quotes = '"';
        #endregion

        #region Predefined Regex
        private Regex Whitespaces
        {
            get
            {
                return new Regex("\\s+");
            }
        }
        #endregion

        #region Implementation Details
        /// <summary>
        /// Formats and splits the tokens by delimiter allowing to add delimiters by quoting
        /// </summary>
        private List<string> _SplitRespectingQuotation()
        {
            string data = _sourceString;

            // Doing some basic transformations
            data = Whitespaces.Replace(data, " ");

            // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
            // Initialisation
            List<string> l = new List<string>();
            char[] record = data.ToCharArray();

            StringBuilder property = new StringBuilder();
            char c;

            bool quoting = false;

            // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
            // Scan character by character
            for (int i = 0; i < record.Length; i++)
            {
                c = record[i];

                // Quotation-Character: Single -> Quote; Double -> Append
                if (c == Quotes)
                {
                    if (i == record.Length - 1)
                    {
                        quoting = !quoting;
                    }
                    else if (Quotes == record[1 + i])
                    {
                        property.Append(c);
                        i++;
                    }
                    else
                    {
                        quoting = !quoting;
                    }
                }

                // Delimiter: Escaping -> Append; Otherwise append
                else if (c == Delimiter)
                {
                    if (quoting)
                    {
                        property.Append(c);
                    }
                    else
                    {
                        l.Add(property.ToString());
                        property.Clear();
                    }
                }

                // Any other character: Append
                else
                {
                    property.Append(c);
                }
            }

            l.Add(property.ToString());                         // Add last token

            // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
            // Checking consistency
            if (quoting) throw new FormatException();          // All open quotation marks closed

            return l;
        }

        /// <summary>
        /// Splits the string by declaring one character as escape
        /// </summary>
        private List<string> _SplitRespectingEscapes()
        {
            string data = _sourceString;

            // Doing some basic transformations
            data = Whitespaces.Replace(data, " ");

            // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
            // Initialisation
            List<string> l = new List<string>();
            char[] record = data.ToCharArray();

            StringBuilder property = new StringBuilder();
            char c;

            bool escaping = false;

            // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
            // Scan character by character
            for (int i = 0; i < record.Length; i++)
            {
                c = record[i];

                if (escaping)
                {
                    property.Append(c);
                    escaping = false;
                    continue;
                }

                if (c == Escape)
                {
                    escaping = true;
                }
                else if (c == Delimiter)
                {
                    l.Add(property.ToString());
                    property.Clear();
                }
                else
                {
                    property.Append(c);
                }
            }

            return l;
        }

        /// <summary>
        /// Splits the string by calling a simple String.Split
        /// </summary>
        private List<string> _SplitPlain()
        {
            return new List<string>(Whitespaces.Replace(_sourceString, " ").Split(Delimiter));
        }

        /// <summary>
        /// Backer for tokens
        /// </summary>
        private List<string> _Tokens
        {
            get
            {
                if (null == _tokens)
                {
                    switch (Strategy)
                    {
                        case (StringTokenizerStrategy.Quotation): _tokens = _SplitRespectingQuotation(); break;
                        case (StringTokenizerStrategy.Escaping): _tokens = _SplitRespectingEscapes(); break;

                        default: _tokens = _SplitPlain(); break;
                    }
                }

                return _tokens;
            }
        }
        private List<string> _tokens = null;
        #endregion
    }

    public enum StringTokenizerStrategy
    {
        Split,
        Quotation,
        Escaping
    }
}

Trả lời

7 TheRubberDuck Sep 19 2020 at 01:42

Tôi không chắc cái này thuộc về một lớp - ít nhất không phải là lớp nào!

Lùi lại một bước và xem điều gì hợp nhất và điều gì tách biệt từng "chiến lược". Tất cả chúng đều cần chuyển đổi một chuỗi đầu vào thành một danh sách các mã thông báo dựa trên một dấu phân cách biến. Tuy nhiên, có những thuộc tính chỉ được sử dụng bởi một trong ba tùy chọn và phần lớn logic phân tách là duy nhất cho chiến lược của nó.

Gợi ý 1: Ba chức năng "độc lập".

Bạn thực sự sẽ phải đặt chúng vào một lớp tĩnh hoặc làm điều gì đó đặc biệt với các ủy viên / lambdas, nhưng cuối cùng thì chẳng thu được gì từ việc có một lớp lớn.

  public static IList<string> SplitRespectingQuotation(string sourceString, char delimiter = ' ', char quote = '"') { ... }
  public static IList<string> SplitRespectingEscapes(string sourceString, char delimiter = ' ', char escape = '\') { ... }
  public static IList<string> SplitPlain(string sourceString, char delimiter = ' ') { ... }

Nếu bạn muốn đầu ra giao tiếp các tham số đầu vào, bạn có thể tạo một lớp nhẹ hơn nhiều để làm như vậy. Thuộc tính của nó sẽ là readonly; nếu bạn cần thay đổi chúng và tính toán lại, chỉ cần gọi lại hàm. Xét cho cùng, đó thực chất là những gì bạn đang làm ở bên trong lớp học hiện tại của mình!

Một điểm cộng khác: nếu và khi bạn đưa ra một chiến lược mới để tách, bạn có thể chỉ cần tạo một hàm mới mà không ảnh hưởng đến các hàm khác. Tất cả chúng đều có thể kiểm tra độc lập, có thể chỉnh sửa và có thể xóa.

Gợi ý 2: Ba lớp cụ thể mở rộng một lớp cơ sở trừu tượng.

Tôi thích những gì bạn đã làm với thuộc _Tokenstính: nó cho phép bạn trì hoãn việc tính toán cho đến khi bạn thực sự cần, điều này rất hữu ích trong những trường hợp bạn không làm như vậy. Ngoài ra, một trường hợp sử dụng mà nó hỗ trợ (không được hỗ trợ bởi các chức năng "độc lập") là thay đổi, ví dụ: ký tự thoát và kết quả tự động bị "vô hiệu hóa".

Để duy trì hành vi đó, bạn có thể kéo các phần tử chung vào một lớp cơ sở trừu tượng, như sau:

public abstract class StringTokenizer
{
  public string SourceString { get; }

  public StringTokenizer(string dataInput)
  {
    SourceString = dataInput ?? string.Empty;
  }

  public string this[int index] => index >= this.Count ? String.Empty : Tokens[index];

  public int Count => Tokens.Count;

  public char Delimiter
  {
    get { return _delimiter; }
    set
    {
      if (value != _delimiter)
      {
         _delimiter = value;
         InvalidateResult();
      }
    }
  }
  private char _delimiter = ' ';

  public IEnumerable<string> Tokens
  {
    get
    {
      if (_tokens is null)
      {
        _tokens = ComputeTokens();
      }
      return _tokens;
    }
  }
  private List<string> _tokens = null;

  protected abstract List<string> ComputeTokens();

  protected void InvalidateResult()
  {
    _tokens = null;
  }
}

Những thay đổi đáng chú ý:

  1. Không có logic phân chia thực tế. Mỗi chiến lược sẽ cung cấp riêng của mình.
  2. Các thuộc tính dành riêng cho chiến lược không có. Không cần chiến lược dựa trên lối thoát để có thuộc tính cho ký tự trích dẫn và ngược lại.
  3. Thay vì thiết lập trực tiếp _tokens = null, các thuộc tính nên gọi InvalidateResult. Điều này cho phép _tokensđược thực hiện privateđể giữ logic được chứa trong lớp cơ sở.
  4. Tokenslà công khai, và là một IEnumerable. Điều này cho phép người tiêu dùng sử dụng foreach, nhưng không khuyến khích sửa đổi trực tiếp.

Một lớp cơ sở bây giờ có chính xác một công việc: thực hiện ComputeTokens. Nếu nó cần tạo thuộc tính để làm như vậy, nó có thể làm như vậy, dựa trên logic chiến lược cụ thể của riêng nó. Nếu các thuộc tính đó cần làm mất hiệu lực các mã thông báo đã tính toán trước đó khi chúng thay đổi, chúng có thể gọi InvalidateResult.

Dưới đây là một ví dụ sơ bộ về lớp con chiến lược sẽ trông như thế nào:

public sealed class EscapeStringTokenizer : StringTokenizer
{
  public EscapeStringTokenizer (string dataInput) : base(dataInput) { }

  public char Escape
  {
    get { return _escape; }
    set
    {
      if (value != _escape)
      {
         _escape = value;
         InvalidateResult();
      }
    }
  }

  protected override List<string> ComputeTokens()
  {
    // Actual logic omitted
  }
}

Các quan sát khác

  1. Bạn cho phép chỉ định dấu phân cách, nhưng bạn luôn cô đặc khoảng trắng. Nếu tôi phân tách "a,a and b,b"bằng dấu phân cách ",", tôi sẽ mong đợi lấy {"a", "a and b", "b"}lại - nhưng thực tế sẽ nhận được {"a", "a and b", "b"}.
  2. Nếu dấu phân cách, v.v., có thể được đọc công khai, tại sao không hiển thị cả chuỗi nguồn? Xem SourceStringlớp trừu tượng của tôi ở trên.
  3. Tôi thấy các trình truy cập thuộc tính dạng biểu thức (tương đối mới) sẽ tốt hơn cho các thuộc tính đơn giản. Xem Counttrong lớp trừu tượng của tôi ở trên.
  4. Tôi không nghĩ có thể vô tình gán nullcho một biến làm điều kiện của câu lệnh if. Điều này là do x = nullđánh giá phải cùng kiểu với x, cần phải là bool(và do đó, không thể null) để là một điều kiện hợp lệ. Nếu bạn vẫn muốn tránh x == null, bạn có thể nói x is null.
  5. Như những người khác đã đề cập, bạn không nên đặt tiền tố thuộc tính với _. Nó không ở đó để phân biệt giữa công khai và riêng tư, mà là giữa các biến cục bộ và các trường lớp. Cá nhân tôi, mặc dù, tôi thậm chí không sử dụng _trong trường hợp đó, nhưng thay vào đó thích hơn this.nếu cần. Nhưng nhìn chung, bạn sẽ cần phải linh hoạt về điều đó và đảm bảo tuân theo bất kỳ khuôn mẫu nào đã được thiết lập trong một nhóm hoặc dự án hiện có.
  6. Cũng như những người khác đã đề cập, sử dụng varkhi khai báo các biến bất cứ khi nào có thể. Bất kỳ IDE nào tốt sẽ có thể cho bạn biết loại khi bạn di chuột qua biến và tên của nó sẽ cho bạn biết nó dùng để làm gì ngay cả khi không có loại.
  7. Lưu ý, hãy tránh những cái tên như cvà l. icũng tốt vì nó có tính thành ngữ như một biến vòng lặp / chỉ mục, nhưng những biến số khác yêu cầu thêm ngữ cảnh để hiểu. Các ký tự mã nguồn rẻ, vì vậy hãy trả tiền để có thêm một số khả năng đọc bằng cách sử dụng currentCharvà finishedTokens.
  8. Bạn không cần phải dịch nguồn stringthành a char[]; bạn đã có thể truy cập các ký tự trong một stringchỉ mục.
9 Heslacher Sep 18 2020 at 16:53
  • Bạn không nên có Whitespacesnhư một thuộc tính chỉ nhận mà là một readonlytrường riêng tư và bạn nên biên dịch regex đó vì bạn đang sử dụng nó khá thường xuyên.

  • Sử dụng regionđược coi là một phản vật chất

  • Chỉ sử dụng tiền tố gạch dưới cho các trường riêng tư. Không sử dụng chúng cho các phương thức hoặc thuộc tính.

  • Nếu loại biến rõ ràng ở phía bên phải của nhiệm vụ, bạn nên sử dụng varthay vì loại cụ thể.

  • Mã đang hoạt động rất nhiều _sourceStringcó thể là string.Emptydo đối số ctor được truyền vào dataInputcó thể là nullhoặc string.Empty. Tôi muốn ném một ngoại lệ vào ctor.

  • Thay vì gán một biến cho một biến khác và sau đó thao tác với biến kết quả, bạn chỉ có thể làm điều đó trên một dòng như vd:

    string data = Whitespaces.Replace(_sourceString, " ");  
    

    thay vì

    string data = _sourceString;
    
    // Doing some basic transformations
    data = Whitespaces.Replace(data, " ");  
    
  • Nếu bạn chỉ cần truy cập mục duy nhất của một mảng và không cần phải nhìn về phía trước, bạn nên chọn một foreachtrong một forvòng lặp.

5 AlexanderPetrov Sep 18 2020 at 19:51
  • Một cái ltên chỉ có một chữ cái có vẻ xấu đối với tôi.

  • Tôi nghĩ bạn nên thêm thông báo vào ngoại lệ mô tả lý do gây ra lỗi.

  • Theo mặc định, bạn xóa tất cả khoảng trắng khỏi dữ liệu. Nhưng chúng có thể cần thiết bên trong các mã thông báo. Bạn có thể thực hiện một tùy chọn bổ sung để chỉ định điều này.

1 Benj Sep 20 2020 at 00:13

Cảm ơn mọi người vì những phản hồi tuyệt vời. Tôi đã áp dụng hầu hết các thay đổi đối với mã của mình đang được lưu trữ dưới dạng FOS trênhttps://github.com/pillepalle1/dotnet-pillepalle1 nơi nó sẽ được bảo trì thêm.

Hiện tại, tôi đã đóng gói logic phân tách thành ba phương thức mở rộng tĩnh. Ngoài ra, tôi đã xây dựng các trình bao bọc theo đề xuất của therubberduck để có thể tùy chọn giữ sự thoải mái của việc vô hiệu mã thông báo tự động

Đề xuất tôi đã thực hiện

  • Đặt tên biến Tên biến chẳng hạn như lđã được thay thế bằng tên mô tả hơn

  • Thông báo ngoại lệ đã được thêm vào

  • Sửa đổi nội dung mã thông báo Đã bị loại bỏ hoàn toàn khỏi các phương thức mở rộng và có sẵn tùy chọn trong trình bao bọc

  • Các khu vực đã bị xóa hoàn toàn

  • Sử dụng var bất cứ khi nào hợp lý / có thể

  • Vòng lặp Thích foreachhơn forcác vòng lặp và lặp lại sourceStringthay vì chuyển đổi nó thành char[]đầu tiên

  • Ném chuỗi nhậpArgumentNullException thay vì chuyển đổi nullthànhString.Empty

  • Tách CSV theo RFC4180

Tôi sẽ áp dụng nhiều thay đổi hơn nhưng một số đề xuất (tức là Whitespacescác thuộc tính liên quan và biểu hiện) đã trở nên lỗi thời trong triển khai mới.

Các đề xuất tôi chưa thực hiện

  • Việc đặt tên gạch dưới cho mọi thứ riêng tư / được bảo vệ có vẻ hợp lý hơn đối với tôi hơn là chỉ phân biệt giữa biến thành viên và biến cục bộ vì khi triển khai các cấu trúc dữ liệu mạnh mẽ đồng thời (đã trở thành một điều quan trọng kể từ khi Tasksđược triển khai). phương thức thực hiện kiểm tra đồng thời (công khai) hay không (riêng tư).

Mã

Phương pháp Tokenizer tĩnh

using System;
using System.Text;
using System.Collections.Immutable;

namespace pillepalle1.Text
{
    public static class StringTokenizer
    {
        private static FormatException _nonQuotedTokenMayNotContainQuotes =
            new FormatException("[RFC4180] If fields are not enclosed with double quotes, then double quotes may not appear inside the fields.");

        private static FormatException _quotesMustBeEscapedException =
            new FormatException("[RFC4180] If double-quotes are used to enclose fields, then a double-quote appearing inside a field must be escaped by preceding it with another double quote.");

        private static FormatException _tokenNotFullyEnclosed =
            new FormatException("[RFC4180] \"Each field may or may not be enclosed in double quotes\". However, for the final field the closing quotes are missing.");


        /// <summary>
        /// <para>
        /// Formats and splits the tokens by delimiter allowing to add delimiters by quoting 
        /// similar to https://tools.ietf.org/html/rfc4180
        /// </para>
        /// 
        /// <para>
        /// Each field may or may not be enclosed in double quotes (however some programs, such as 
        /// Microsoft Excel, do not use double quotes at all). If fields are not enclosed with 
        /// double quotes, then double quotes may not appear inside the fields.
        /// </para>
        /// 
        /// <para>
        /// Fields containing line breaks (CRLF), double quotes, and commas should be enclosed in 
        /// double-quotes.
        /// </para>
        /// 
        /// <para>
        /// If double-quotes are used to enclose fields, then a double-quote appearing inside a 
        /// field must be escaped by preceding it with another double quote.
        /// </para>
        /// 
        /// <para>
        /// The ABNF defines 
        /// 
        /// [field = (escaped / non-escaped)] ||  
        /// [non-escaped = *TEXTDATA] || 
        /// [TEXTDATA =  %x20-21 / %x23-2B / %x2D-7E]
        /// 
        /// specifically forbidding to include quotes in non-escaped fields, hardening the *SHOULD*
        /// requirement above.
        /// </para>
        /// </summary>
        public static ImmutableList<string> SplitRespectingQuotation(this string sourceString, char delimiter = ' ', char quotes = '"')
        {
            // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
            // Initialisation
            var tokenList = ImmutableList<string>.Empty;
            var tokenBuilder = new StringBuilder();

            var expectingDelimiterOrQuotes = false;     // Next char must be Delimiter or Quotes
            var hasReadTokenChar = false;               // We are not between tokens (=> No quoting)
            var isQuoting = false;

            // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
            // Scan character by character
            foreach (char c in sourceString)
            {
                if (expectingDelimiterOrQuotes)
                {
                    expectingDelimiterOrQuotes = false;

                    if (c == delimiter)
                    {
                        isQuoting = false;
                    }

                    else if (c == quotes)
                    {
                        tokenBuilder.Append(c);
                        hasReadTokenChar = true;
                        continue;
                    }

                    else
                    {
                        throw _quotesMustBeEscapedException;
                    }
                }

                // -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

                if (c == quotes)
                {
                    if (isQuoting)
                    {
                        expectingDelimiterOrQuotes = true;
                    }

                    else
                    {
                        if (hasReadTokenChar)
                        {
                            throw _nonQuotedTokenMayNotContainQuotes;
                        }

                        isQuoting = true;
                    }
                }

                else if (c == delimiter)
                {
                    if (isQuoting)
                    {
                        tokenBuilder.Append(c);
                        hasReadTokenChar = true;
                    }
                    else
                    {
                        tokenList = tokenList.Add(tokenBuilder.ToString());
                        tokenBuilder.Clear();
                        hasReadTokenChar = false;
                    }
                }

                // Any other character is just being appended to
                else
                {
                    tokenBuilder.Append(c);
                    hasReadTokenChar = true;
                }
            }

            // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
            // Tidy up open flags and checking consistency

            tokenList = tokenList.Add(tokenBuilder.ToString());

            if (isQuoting && !expectingDelimiterOrQuotes)
            {
                throw _tokenNotFullyEnclosed;
            }

            return tokenList;
        }

        /// <summary>
        /// Splits the string by declaring one character as escape
        /// </summary>
        public static ImmutableList<string> SplitRespectingEscapes(this string sourceString, char delimiter = ' ', char escapeChar = '\\')
        {
            // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
            // Initialisation
            var tokenList = ImmutableList<string>.Empty;
            var tokenBuilder = new StringBuilder();

            var escapeNext = false;

            // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
            // Scan character by character
            foreach (char c in sourceString)
            {
                if (escapeNext)
                {
                    tokenBuilder.Append(c);
                    escapeNext = false;
                    continue;
                }

                if (c == escapeChar)
                {
                    escapeNext = true;
                }
                else if (c == delimiter)
                {
                    tokenList = tokenList.Add(tokenBuilder.ToString());
                    tokenBuilder.Clear();
                }
                else
                {
                    tokenBuilder.Append(c);
                }
            }

            // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
            // Tidy up open flags and checking consistency
            tokenList = tokenList.Add(tokenBuilder.ToString());

            if (escapeNext) throw new FormatException();            // Expecting additional char


            return tokenList;
        }

        /// <summary>
        /// Splits the string by calling a simple String.Split
        /// </summary>
        public static ImmutableList<string> SplitPlain(this string sourceString, char delimiter = ' ')
        {
            return ImmutableList<string>.Empty.AddRange(sourceString.Split(delimiter));
        }
    }
}

Lớp cơ sở trình bao bọc trừu tượng

using System;
using System.Collections.Immutable;

namespace pillepalle1.Text
{
    public abstract class AStringTokenizer
    {
        public AStringTokenizer()
        {

        }

        public AStringTokenizer(string sourceString)
        {
            SourceString = sourceString;
        }

        /// <summary>
        /// String that is supposed to be split in tokens
        /// </summary>
        public string SourceString
        {
            get
            {
                return _sourceString;
            }
            set
            {
                if (null == value)
                {
                    throw new ArgumentNullException("Cannot split null in tokens");
                }
                else if (_sourceString.Equals(value))
                {
                    // nop
                }
                else
                {
                    _sourceString = value;
                    _InvalidateTokens();
                }
            }
        }
        private string _sourceString = String.Empty;

        /// <summary>
        /// Character indicating how the source string is supposed to be split
        /// </summary>
        public char Delimiter
        {
            get
            {
                return _delimiter;
            }
            set
            {
                if (value != _delimiter)
                {
                    _delimiter = value;
                    _InvalidateTokens();
                }
            }
        }
        private char _delimiter = ' ';

        /// <summary>
        /// Flag indicating whether whitespaces should be removed from start and end of each token
        /// </summary>
        public bool TrimTokens
        {
            get
            {
                return _trimTokens;
            }
            set
            {
                if (value != _trimTokens)
                {
                    _trimTokens = value;
                    _InvalidateTokens();
                }
            }
        }
        private bool _trimTokens = false;

        /// <summary>
        /// Result of tokenization
        /// </summary>
        public ImmutableList<string> Tokens
        {
            get
            {
                if (null == _tokens)
                {
                    _tokens = Tokenize();

                    if (TrimTokens)
                    {
                        _tokens = _TrimTokens(_tokens);
                    }
                }

                return _tokens;
            }
        }
        private ImmutableList<string> _tokens = null;

        /// <summary>
        /// Split SourceString into tokens
        /// </summary>
        protected abstract ImmutableList<string> Tokenize();

        /// <summary>
        /// Trims whitespaces from tokens
        /// </summary>
        /// <param name="candidates">List of tokens</param>
        private ImmutableList<string> _TrimTokens(ImmutableList<string> candidates)
        {
            var trimmedTokens = ImmutableList<string>.Empty;

            foreach (var token in candidates)
            {
                trimmedTokens = trimmedTokens.Add(token.Trim());
            }

            return trimmedTokens;
        }

        /// <summary>
        /// Invalidate and recompute tokens if necessary
        /// </summary>
        protected void _InvalidateTokens()
        {
            _tokens = null;
        }
    }
}

Wrapper để mã hóa đơn giản

using System.Collections.Immutable;

namespace pillepalle1.Text
{
    public class PlainStringTokenizer : AStringTokenizer
    {
        protected override ImmutableList<string> Tokenize()
        {
            return SourceString.SplitPlain(Delimiter);
        }
    }
}

Wrapper để mã hóa báo giá

using System.Collections.Immutable;

namespace pillepalle1.Text
{
    public class QuotationStringTokenizer : AStringTokenizer
    {
        /// <summary>
        /// Indicates which character is used to encapsulate tokens
        /// </summary>
        public char Quotes
        {
            get
            {
                return _quotes;
            }
            set
            {
                if (value != _quotes)
                {
                    _quotes = value;
                    _InvalidateTokens();
                }
            }
        }
        private char _quotes = '"';

        protected override ImmutableList<string> Tokenize()
        {
            return SourceString.SplitRespectingQuotation(Delimiter, Quotes);
        }
    }
}

Wrapper để mã hóa thoát

using System.Collections.Immutable;

namespace pillepalle1.Text
{
    public class EscapedStringTokenizer : AStringTokenizer
    {
        /// <summary>
        /// Indicates which character is used to escape characters
        /// </summary>
        public char Escape
        {
            get
            {
                return _escape;
            }
            set
            {
                if (value != _escape)
                {
                    _escape = value;
                    _InvalidateTokens();
                }
            }
        }
        private char _escape = '"';

        protected override ImmutableList<string> Tokenize()
        {
            return SourceString.SplitRespectingEscapes(Delimiter, Escape);
        }
    }
}