Tại sao độ dài của chuỗi này dài hơn số lượng ký tự trong nó?


145

Mã này:

string a = "abc";
string b = "A𠈓C";
Console.WriteLine("Length a = {0}", a.Length);
Console.WriteLine("Length b = {0}", b.Length);

đầu ra:

Length a = 3
Length b = 4

Tại sao? Điều duy nhất tôi có thể tưởng tượng là ký tự tiếng Trung dài 2 byte và .Lengthphương thức trả về số byte.


10
Làm thế nào tôi biết đó là một vấn đề cặp thay thế chỉ từ việc nhìn vào tiêu đề. Ah, hệ thống 'ol tốt. Cân bằng là đồng minh của bạn!
Chris Cirefice

9
nó dài 4 byte trong UTF-16, không phải 2
phuclv

giá trị thập phân của char 𠈓là 131603 và vì các ký tự là các byte không dấu, điều đó có nghĩa là bạn có thể đạt được giá trị đó trong 2 ký tự thay vì 4 (tối đa giá trị 16 bit không dấu là 65535 (hoặc 65536 biến thể) và sử dụng 2 ký tự để biểu thị cho phép cho số lượng biến thể tối đa không phải là 65536 * 2 (131072) mà là 65536 * 65536 biến thể (4.294.967.296, có giá trị 32 bit)
GMasucci

3
@GMAsucci: Đó là 2 ký tự trong UTF-16, nhưng 4 byte, vì một ký tự UTF16 có kích thước 2 byte, nếu không, nó không thể lưu trữ 65536 biến thể, nhưng chỉ 256.
Kaiserludi

4
Tôi khuyên bạn nên đọc bài viết tuyệt vời 'The Absolute tối thiểu Mỗi Software Developer Tuyệt đối, tích cực Phải Biết Về Unicode và tự Sets (Không Lý Do!)' Joelonsoftware.com/articles/Unicode.html
ItsMe

Câu trả lời:


232

Mọi người khác đang đưa ra câu trả lời bề ngoài, nhưng cũng có một lý do sâu xa hơn: số lượng "ký tự" là một câu hỏi khó xác định và có thể tốn kém đáng ngạc nhiên để tính toán, trong khi một thuộc tính có độ dài phải nhanh.

Tại sao nó khó định nghĩa? Chà, có một vài lựa chọn và không có lựa chọn nào thực sự hợp lệ hơn cái khác:

  • Số lượng đơn vị mã (byte hoặc khối dữ liệu có kích thước cố định khác; C # và Windows thường sử dụng UTF-16 để nó trả về số lượng các mảnh hai byte) chắc chắn có liên quan, vì máy tính vẫn cần xử lý dữ liệu ở dạng đó cho nhiều mục đích (ví dụ, ghi vào một tệp, quan tâm đến byte hơn là ký tự)

  • Số lượng điểm mã Unicode khá dễ tính (mặc dù O (n) vì bạn phải quét chuỗi cho các cặp thay thế) và có thể quan trọng đối với trình soạn thảo văn bản .... nhưng thực tế không giống với số lượng ký tự in trên màn hình (gọi là đồ thị). Ví dụ: một số chữ cái có dấu có thể được thể hiện dưới hai hình thức: một mật mã đơn hoặc hai điểm được ghép với nhau, một điểm đại diện cho chữ cái và một điểm nói "thêm dấu vào thư đối tác của tôi". Cặp đôi sẽ là hai nhân vật hay một? Bạn có thể chuẩn hóa các chuỗi để trợ giúp điều này, nhưng không phải tất cả các chữ cái hợp lệ đều có một biểu diễn mã.

  • Ngay cả số lượng biểu đồ không giống với độ dài của chuỗi in, điều này phụ thuộc vào phông chữ trong số các yếu tố khác và do một số ký tự được in với một số chồng chéo trong nhiều phông chữ (k sâu), độ dài của chuỗi trên màn hình không nhất thiết phải bằng tổng chiều dài của đồ thị nào!

  • Một số điểm Unicode thậm chí không có ký tự theo nghĩa truyền thống, mà là một số loại dấu kiểm soát. Giống như một điểm đánh dấu thứ tự byte hoặc chỉ báo từ phải sang trái. Làm những tính này?

Nói tóm lại, độ dài của một chuỗi thực sự là một câu hỏi phức tạp đến nực cười và việc tính toán nó có thể tốn rất nhiều thời gian của CPU cũng như các bảng dữ liệu.

Hơn nữa, vấn đề là gì? Tại sao các số liệu này quan trọng? Vâng, chỉ có bạn có thể trả lời rằng cho trường hợp của bạn, nhưng cá nhân tôi, tôi thấy họ nói chung là không liên quan. Giới hạn nhập dữ liệu tôi thấy được thực hiện hợp lý hơn bởi các giới hạn byte, vì đó là những gì cần phải được chuyển hoặc lưu trữ bằng mọi cách. Giới hạn kích thước hiển thị được thực hiện tốt hơn bởi phần mềm bên hiển thị - nếu bạn có 100 pixel cho tin nhắn, số lượng ký tự bạn phù hợp phụ thuộc vào phông chữ, v.v., dù sao phần mềm lớp dữ liệu không biết. Cuối cùng, do sự phức tạp của tiêu chuẩn unicode, có lẽ bạn sẽ gặp lỗi ở các trường hợp cạnh nếu bạn thử bất cứ điều gì khác.

Vì vậy, nó là một câu hỏi khó với không sử dụng nhiều mục đích chung. Số lượng đơn vị mã là không đáng kể để tính toán - nó chỉ là độ dài của mảng dữ liệu cơ bản - và có ý nghĩa / hữu ích nhất như một quy tắc chung, với một định nghĩa đơn giản.

Đó là lý do tại sao bcó độ dài 4vượt quá lời giải thích bề mặt của "bởi vì tài liệu nói như vậy".


9
Về cơ bản '.Lipse' không phải là điều mà hầu hết các lập trình viên nghĩ. Có lẽ nên có một tập hợp các thuộc tính cụ thể hơn (ví dụ: GlyphCount) và Độ dài được đánh dấu là lỗi thời!
redcalx

8
@locster Tôi đồng ý, nhưng đừng nghĩ Lengthnên lỗi thời, để duy trì sự tương tự với các mảng.
Kroltan

2
@locster Không nên lỗi thời. Con trăn có rất nhiều ý nghĩa và không ai thắc mắc điều đó.
simonzack

1
Tôi nghĩ. Chiều dài có rất nhiều ý nghĩa và là một tài sản tự nhiên, miễn là bạn hiểu nó là gì và tại sao nó lại như vậy. Sau đó, nó hoạt động như bất kỳ mảng nào khác (trong một số ngôn ngữ như D, một chuỗi theo nghĩa đen là một mảng theo như ngôn ngữ có liên quan và nó hoạt động rất tốt)
Adam D. Ruppe

4
Điều đó không đúng (một quan niệm sai lầm phổ biến) - với UTF-32, lengthInBytes / 4 sẽ cho số lượng điểm mã , nhưng nó không giống với số lượng "ký tự" hoặc biểu đồ. Hãy xem xét LATIN SMALL LETTER E theo sau là một DIAERESIS COMBINING ... in dưới dạng một ký tự, nó thậm chí có thể được chuẩn hóa thành một mật mã duy nhất, nhưng nó vẫn dài hai đơn vị, ngay cả trong UTF-32.
Adam D. Ruppe

62

Từ các tài liệu của String.Lengthtài sản:

Thuộc tính Độ dài trả về số lượng đối tượng Char trong trường hợp này, không phải số lượng ký tự Unicode. Lý do là một ký tự Unicode có thể được đại diện bởi nhiều hơn một Char . Sử dụng lớp System.Globalization.StringInfo để làm việc với từng ký tự Unicode thay vì mỗi Char .


3
Java hoạt động theo cùng một cách (cũng in 4 cho String b), vì nó sử dụng biểu diễn UTF-16 trong mảng char. Đó là một ký tự 4 byte trong UTF-8.
Michael

32

Nhân vật của bạn ở chỉ số 1 in "A𠈓C"SurrogatePair

Điểm quan trọng cần nhớ là các cặp thay thế đại diện cho các ký tự đơn 32 bit .

Bạn có thể thử mã này và nó sẽ trở lại True

Console.WriteLine(char.IsSurrogatePair("A𠈓C", 1));

Phương thức Char.IsSurrogatePair (Chuỗi, Int32)

truenếu tham số s bao gồm các ký tự liền kề tại chỉ mục vị trí và chỉ mục + 1 và giá trị số của ký tự ở chỉ số vị trí nằm trong khoảng từ U + D800 đến U + DBFF và giá trị số của ký tự ở chỉ số vị trí + 1 phạm vi từ U + DC00 qua U + DFFF; nếu không false.

Điều này được giải thích thêm trong thuộc tính String.Lipse :

Thuộc tính Độ dài trả về số lượng đối tượng Char trong trường hợp này, không phải số lượng ký tự Unicode. Lý do là một ký tự Unicode có thể được đại diện bởi nhiều hơn một Char. Sử dụng lớp System.Globalization.StringInfo để làm việc với từng ký tự Unicode thay vì mỗi Char.


24

Như các câu trả lời khác đã chỉ ra, ngay cả khi có 3 ký tự hiển thị, chúng được đại diện với 4 charđối tượng. Đó là lý do tại sao Lengthlà 4 chứ không phải 3.

MSDN nói rằng

Thuộc tính Độ dài trả về số lượng đối tượng Char trong trường hợp này, không phải số lượng ký tự Unicode.

Tuy nhiên, nếu điều bạn thực sự muốn biết là số lượng "phần tử văn bản" chứ không phải số lượng Charđối tượng bạn có thể sử dụng StringInfolớp.

var si = new StringInfo("A𠈓C");
Console.WriteLine(si.LengthInTextElements); // 3

Bạn cũng có thể liệt kê từng yếu tố văn bản như thế này

var enumerator = StringInfo.GetTextElementEnumerator("A𠈓C");
while(enumerator.MoveNext()){
    Console.WriteLine(enumerator.Current);
}

Sử dụng foreachtrên chuỗi sẽ phân chia "chữ cái" ở giữa thành hai charđối tượng và kết quả được in sẽ không tương ứng với chuỗi.


20

Đó là bởi vì thuộc Lengthtính trả về số lượng đối tượng char , không phải số lượng ký tự unicode. Trong trường hợp của bạn, một trong các ký tự Unicode được đại diện bởi nhiều hơn một đối tượng char (SurrogatePair).

Thuộc tính Độ dài trả về số lượng đối tượng Char trong trường hợp này, không phải số lượng ký tự Unicode. Lý do là một ký tự Unicode có thể được đại diện bởi nhiều hơn một Char. Sử dụng lớp System.Globalization.StringInfo để làm việc với từng ký tự Unicode thay vì mỗi Char.


1
Bạn có cách sử dụng "ký tự" mơ hồ trong câu trả lời này. Tôi đề nghị thay thế ít nhất là cái đầu tiên bằng thuật ngữ chính xác.
Các cuộc đua nhẹ nhàng trong quỹ đạo

1
Cảm ơn bạn. Đã sửa lỗi mơ hồ.
Yuval Itzchakov

10

Như những người khác đã nói, đó không phải là số lượng ký tự trong chuỗi mà là số lượng đối tượng Char. Ký tự là điểm mã U + 20213. Vì giá trị nằm ngoài phạm vi char loại 16 bit, nên nó được mã hóa theo UTF-16 dưới dạng cặp thay thế D840 DE13.

Cách để có được độ dài trong các ký tự đã được đề cập trong các câu trả lời khác. Tuy nhiên, nó nên được sử dụng cẩn thận vì có thể có nhiều cách để thể hiện một ký tự bằng Unicode. "à" có thể là 1 ký tự sáng tác hoặc 2 ký tự (a + dấu phụ). Bình thường hóa có thể cần thiết như trong trường hợp của twitter .

Bạn nên đọc điều này
Tối thiểu tuyệt đối Mỗi nhà phát triển phần mềm Tuyệt đối, Tích cực phải biết về Unicode và Bộ ký tự (Không có lý do!)


6

Điều này là do length()chỉ hoạt động đối với các điểm mã Unicode không lớn hơn U+FFFF. Tập hợp các điểm mã này được gọi là Mặt phẳng đa ngôn ngữ cơ bản (BMP) và chỉ sử dụng 2 byte.

Các điểm mã Unicode bên ngoài BMPđược biểu diễn trong UTF-16 bằng cách sử dụng các cặp thay thế 4 byte.

Để đếm chính xác số lượng ký tự (3), hãy sử dụng StringInfo

StringInfo b = new StringInfo("A𠈓C");
Console.WriteLine(string.Format("Length 2 = {0}", b.LengthInTextElements));

6

Được rồi, trong .Net và C #, tất cả các chuỗi được mã hóa dưới dạng UTF-16LE . A stringđược lưu trữ dưới dạng một chuỗi ký tự. Mỗi chargói đóng gói lưu trữ 2 byte hoặc 16 bit.

Những gì chúng ta thấy "trên giấy hoặc màn hình" là một chữ cái, ký tự, glyph, ký hiệu hoặc dấu chấm câu có thể được coi là một thành phần văn bản duy nhất. Như được mô tả trong Phụ lục Unicode # 29 PHÂN TÍCH VĂN BẢN UNICODE , mỗi Phần tử Văn bản được biểu thị bằng một hoặc nhiều Điểm Mã. Một danh sách đầy đủ các Mã có thể được tìm thấy ở đây .

Mỗi Điểm Mã cần được mã hóa thành nhị phân để biểu diễn bên trong bằng máy tính. Như đã nêu, mỗi charcửa hàng 2 byte. Mã điểm tại hoặc bên dưới U+FFFFcó thể được lưu trữ trong một char. Điểm Mã ở trên U+FFFFđược lưu trữ dưới dạng cặp thay thế, sử dụng hai ký tự đại diện cho một Điểm Mã duy nhất.

Dựa vào những gì chúng ta biết bây giờ chúng ta có thể suy ra, một Phần tử văn bản có thể được lưu trữ dưới dạng một char, như một cặp thay thế của hai ký tự hoặc, nếu Phần tử văn bản được biểu thị bằng nhiều Điểm mã kết hợp một số ký tự đơn và Cặp thay thế. Như thể điều đó không đủ phức tạp, một số Thành phần Văn bản có thể được biểu diễn bằng các kết hợp Điểm Mã khác nhau như được mô tả trong, Phụ lục Chuẩn Unicode # 15, HÌNH THỨC BÌNH LUẬN BÌNH LUẬN UNICODE .


Kết hợp

Vì vậy, các chuỗi trông giống nhau khi được kết xuất thực sự có thể được tạo thành từ một tổ hợp ký tự khác nhau. Một so sánh thứ tự (byte theo byte) của hai chuỗi như vậy sẽ phát hiện ra sự khác biệt, điều này có thể là bất ngờ hoặc không mong muốn.

Bạn có thể mã hóa lại chuỗi .Net. để họ sử dụng cùng một hình thức chuẩn hóa. Sau khi được chuẩn hóa, hai chuỗi có cùng các thành phần văn bản sẽ được mã hóa theo cùng một cách. Để làm điều này, sử dụng hàm string.N normalize . Tuy nhiên, hãy nhớ rằng, một số yếu tố văn bản khác nhau trông tương tự nhau. :-S


Vì vậy, tất cả điều này có nghĩa gì liên quan đến câu hỏi? Phần tử văn bản '𠈓'được biểu thị bằng phần mở rộng chữ tượng hình thống nhất U + 20213 cjk b . Điều này có nghĩa là nó không thể được mã hóa thành một lần duy nhất charvà phải được mã hóa thành Cặp thay thế, sử dụng hai ký tự. Đây là lý do tại sao string bmột trong những charlâu hơn đó string a.

Nếu bạn cần tin cậy (xem cảnh báo), hãy đếm số lượng Thành phần Văn bản trong một stringbạn nên sử dụng System.Globalization.StringInfolớp như thế này.

using System.Globalization;

string a = "abc";
string b = "A𠈓C";

Console.WriteLine("Length a = {0}", new StringInfo(a).LengthInTextElements);
Console.WriteLine("Length b = {0}", new StringInfo(b).LengthInTextElements);

đưa ra đầu ra,

"Length a = 3"
"Length b = 3"

như mong đợi.


Hãy cẩn thận

Việc triển khai .Net của Phân đoạn văn bản Unicode trong StringInfoTextElementEnumeratorcác lớp nói chung sẽ hữu ích và, trong hầu hết các trường hợp, sẽ mang lại phản hồi mà người gọi mong đợi. Tuy nhiên, như đã nêu trong Phụ lục tiêu chuẩn Unicode # 29, "Mục tiêu phù hợp với nhận thức của người dùng không phải lúc nào cũng có thể được đáp ứng chính xác bởi vì văn bản không phải lúc nào cũng chứa đủ thông tin để quyết định ranh giới rõ ràng."


Tôi nghĩ rằng câu trả lời của bạn có khả năng gây nhầm lẫn. Trong trường hợp này, chỉ là một điểm mã duy nhất, nhưng vì điểm mã của nó vượt quá 0xFFFF, nên nó phải được biểu diễn dưới dạng 2 đơn vị mã bằng cách sử dụng cặp thay thế. Grapheme là một khái niệm khác được xây dựng dựa trên điểm mã, trong đó biểu đồ có thể được biểu thị bằng một điểm mã hoặc nhiều điểm mã, như được thấy trong Hangul của Hàn Quốc hoặc nhiều ngôn ngữ gốc Latinh.
nhahtdh

@nhahtdh, tôi đồng ý, câu trả lời của tôi là sai. Tôi đã viết lại nó và hy vọng nó bây giờ tạo ra sự rõ ràng hơn.
Jodrell
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.