เนื่องจากสตริงนั้นไม่เปลี่ยนแปลงใน. NET ฉันจึงสงสัยว่าทำไมพวกเขาจึงถูกออกแบบมาอย่างนั้น string.Substring()
ใช้เวลา O ( substring.Length
) แทนO(1)
?
เช่นอะไรคือการแลกเปลี่ยนถ้ามี?
เนื่องจากสตริงนั้นไม่เปลี่ยนแปลงใน. NET ฉันจึงสงสัยว่าทำไมพวกเขาจึงถูกออกแบบมาอย่างนั้น string.Substring()
ใช้เวลา O ( substring.Length
) แทนO(1)
?
เช่นอะไรคือการแลกเปลี่ยนถ้ามี?
คำตอบ:
อัปเดต: ฉันชอบคำถามนี้มากฉันเพิ่ง blogged ดูสตริงการเปลี่ยนแปลงไม่ได้และการคงอยู่
คำตอบสั้น ๆ คือ: O (n) คือ O (1) ถ้า n ไม่ใหญ่ขึ้น คนส่วนใหญ่แยกย่อยเล็ก ๆ จากสายเล็ก ๆ ดังนั้นวิธีการที่ซับซ้อนเติบโต asymptotically เป็นที่ไม่เกี่ยวข้องอย่างสมบูรณ์
คำตอบที่ยาวคือ:
โครงสร้างข้อมูลที่ไม่เปลี่ยนรูปซึ่งสร้างขึ้นเพื่อให้การดำเนินการบนอินสแตนซ์อนุญาตให้ใช้หน่วยความจำของต้นฉบับซ้ำได้ในจำนวนเล็กน้อย (โดยทั่วไปคือ O (1) หรือ O (lg n)) ของการคัดลอกหรือการจัดสรรใหม่เรียกว่า "ถาวร" โครงสร้างข้อมูลที่ไม่เปลี่ยนรูป สตริงใน. NET ไม่เปลี่ยนรูป คำถามของคุณเป็นหลัก "ทำไมพวกเขาไม่ขัดขืน"?
เนื่องจากเมื่อคุณดูการดำเนินการที่โดยปกติแล้วจะทำกับสตริงในโปรแกรม. NET มันเป็นไปในทางที่เกี่ยวข้องแทบจะไม่เลวเลยที่จะสร้างสตริงใหม่ทั้งหมด ค่าใช้จ่ายและความยากลำบากในการสร้างโครงสร้างข้อมูลแบบถาวรที่ซับซ้อนไม่ได้จ่ายเอง
โดยทั่วไปผู้คนใช้ "substring" เพื่อแยกสตริงสั้น ๆ - พูดอักขระสิบหรือยี่สิบ - จากสตริงที่ค่อนข้างยาว - อาจเป็นสองร้อยตัวอักษร คุณมีบรรทัดข้อความในไฟล์ที่คั่นด้วยเครื่องหมายจุลภาคและคุณต้องการแยกฟิลด์ที่สามซึ่งเป็นนามสกุล บรรทัดอาจจะยาวสองร้อยตัวอักษรชื่อจะเป็นสองสามโหล การจัดสรรสตริงและการคัดลอกหน่วยความจำห้าสิบไบต์มีความรวดเร็วอย่างน่าอัศจรรย์บนฮาร์ดแวร์ที่ทันสมัย การสร้างโครงสร้างข้อมูลใหม่ที่ประกอบด้วยตัวชี้ไปยังกึ่งกลางของสตริงที่มีอยู่บวกกับความยาวนั้นยังเร็วอย่างน่าอัศจรรย์ไม่เกี่ยวข้อง "เร็วพอ" คือคำจำกัดความเร็วพอ
โดยทั่วไปแล้วสารสกัดที่แยกออกมาจะมีขนาดเล็กและมีอายุการใช้งานสั้น ตัวเก็บขยะกำลังจะเรียกคืนพวกเขาในไม่ช้าและพวกเขาไม่ได้ใช้พื้นที่มากในกองตั้งแต่แรก ดังนั้นการใช้กลยุทธ์แบบถาวรที่กระตุ้นการใช้งานซ้ำส่วนใหญ่ของหน่วยความจำก็ไม่ชนะเช่นกัน สิ่งที่คุณทำคือทำให้ตัวเก็บขยะของคุณช้าลงเพราะตอนนี้คุณต้องกังวลเกี่ยวกับการจัดการตัวชี้ภายใน
หากการดำเนินการของสายอักขระย่อยที่คนทั่วไปทำกับสตริงนั้นแตกต่างไปจากเดิมอย่างสิ้นเชิงมันน่าจะเป็นไปได้ที่จะใช้วิธีถาวร หากคนทั่วไปมีสตริงอักขระเป็นล้านและแยกสตริงย่อยที่ซ้อนกันหลายพันรายการด้วยขนาดในช่วงหนึ่งแสนอักขระและสตริงย่อยเหล่านั้นอาศัยอยู่เป็นเวลานานบนฮีปดังนั้นจึงเหมาะสมอย่างยิ่งที่จะใช้สตริงย่อยแบบต่อเนื่อง วิธีการ; มันจะสิ้นเปลืองและไม่โง่ แต่โปรแกรมเมอร์สายงานธุรกิจส่วนใหญ่ไม่ทำอะไรเลยแม้แต่สิ่งที่คลุมเครือเช่นนั้น. .NET ไม่ใช่แพลตฟอร์มที่ได้รับการปรับให้เหมาะกับความต้องการของโครงการจีโนมมนุษย์ โปรแกรมเมอร์วิเคราะห์ดีเอ็นเอต้องแก้ปัญหาเกี่ยวกับลักษณะการใช้งานสตริงเหล่านั้นทุกวัน อัตราต่อรองเป็นสิ่งที่ดีที่คุณทำไม่ได้ ผู้ที่สร้างโครงสร้างข้อมูลถาวรของตนเองที่ตรงกับสถานการณ์การใช้งานของตนเอง
ตัวอย่างเช่นทีมของฉันเขียนโปรแกรมที่ทำการวิเคราะห์แบบทันทีของรหัส C # และ VB เมื่อคุณพิมพ์ ไฟล์โค้ดเหล่านี้บางไฟล์มีขนาดใหญ่มากและทำให้เราไม่สามารถทำการจัดการสตริง O (n) เพื่อแยกสตริงย่อยหรือแทรกหรือลบอักขระได้ เราได้สร้างเครือถาวรโครงสร้างข้อมูลไม่เปลี่ยนรูปสำหรับตัวแทนแก้ไขบัฟเฟอร์ข้อความที่อนุญาตให้เราได้อย่างรวดเร็วและมีประสิทธิภาพอีกครั้งใช้เป็นกลุ่มของข้อมูลสตริงที่มีอยู่และที่มีอยู่ในการวิเคราะห์คำศัพท์และประโยคเมื่อแก้ไขทั่วไป นี่เป็นปัญหาที่ยากในการแก้และโซลูชันได้รับการปรับให้เข้ากับโดเมนเฉพาะของการแก้ไขรหัส C # และ VB มันจะไม่สมจริงที่คาดว่าจะมีประเภทสตริงในตัวเพื่อแก้ปัญหานี้สำหรับเรา
string contents = File.ReadAllText(filename); foreach (string line in content.Split("\n")) ...
หรือเวอร์ชั่นอื่น ๆ ฉันหมายถึงอ่านไฟล์ทั้งหมดแล้วประมวลผลส่วนต่าง ๆ การเรียงลำดับของรหัสนั้นจะเร็วกว่ามากและต้องการหน่วยความจำน้อยกว่าถ้าสตริงนั้นขัดขืน คุณจะมีไฟล์หนึ่งสำเนาในหน่วยความจำเสมอแทนที่จะคัดลอกแต่ละบรรทัดจากนั้นส่วนต่าง ๆ ของแต่ละบรรทัดจะดำเนินการ อย่างไรก็ตามอย่างที่ Eric พูด - นั่นไม่ใช่กรณีการใช้งานทั่วไป
String
ถูกนำมาใช้เป็นโครงสร้างข้อมูลถาวร (ที่ไม่ได้ระบุไว้ในมาตรฐาน แต่การใช้งานทั้งหมดที่ฉันรู้ทำ)
แม่นยำเพราะสตริงไม่เปลี่ยนรูป.Substring
ต้องทำสำเนาอย่างน้อยส่วนหนึ่งของสตริงต้นฉบับ การทำสำเนาของnไบต์ควรใช้เวลา O (n)
คุณคิดว่าคุณจะคัดลอกจำนวนไบต์ในเวลาคงที่ได้อย่างไร
แก้ไข: Mehrdad ไม่แนะนำให้คัดลอกสตริงเลย แต่อ้างอิงถึงชิ้นส่วนของมัน
ลองพิจารณาใน. Net ซึ่งเป็นสตริงที่มีหลายเมกะไบต์ซึ่งบางคนเรียก.SubString(n, n+3)
(สำหรับ n ใด ๆ ที่อยู่ตรงกลางของสตริง)
ตอนนี้สตริงทั้งหมดไม่สามารถรวบรวมขยะได้เพียงเพราะมีการอ้างอิงเดียวที่เก็บไว้ที่ 4 ตัวอักษร? ดูเหมือนว่าจะเป็นการสิ้นเปลืองพื้นที่ไร้สาระ
นอกจากนี้การติดตามการอ้างอิงถึงสารตั้งต้น (ซึ่งอาจอยู่ภายในสารตั้งต้น) และพยายามทำสำเนาในเวลาที่เหมาะสมเพื่อหลีกเลี่ยงการเอาชนะ GC (ดังที่อธิบายไว้ข้างต้น) ทำให้แนวคิดเป็นฝันร้าย มันง่ายกว่าและน่าเชื่อถือมากกว่าในการคัดลอก.SubString
และรักษาโมเดลที่ไม่เปลี่ยนแปลงอย่างตรงไปตรงมา
แก้ไข: นี่เป็นเพียงการอ่านเล็ก ๆ น้อย ๆเกี่ยวกับอันตรายของการเก็บการอ้างอิงถึงสตริงย่อยภายในสตริงขนาดใหญ่
memcpy
ซึ่งยังคงเป็น O (n)
char*
สตริงย่อย
NULL
ถูกยกเลิก ตามที่อธิบายไว้ในโพสต์ของ Lippert , 4 ไบต์แรกมีความยาวของสตริง นั่นเป็นเหตุผลที่ Skeet ชี้ให้เห็นว่าพวกเขาสามารถมี\0
ตัวละครได้
Java (ตรงกันข้ามกับ. NET) มีสองวิธีในการทำSubstring()
คุณสามารถพิจารณาว่าคุณต้องการเก็บไว้เป็นข้อมูลอ้างอิงหรือคัดลอกซับสตริงทั้งหมดไปยังตำแหน่งหน่วยความจำใหม่
การ.substring(...)
แชร์char
อาร์เรย์ที่ใช้ภายในอย่างง่ายกับวัตถุ String ดั้งเดิมซึ่งคุณnew String(...)
สามารถคัดลอกไปยังอาร์เรย์ใหม่ได้หากจำเป็น (เพื่อหลีกเลี่ยงการขัดขวางการรวบรวมขยะของต้นฉบับ)
ฉันคิดว่าความยืดหยุ่นแบบนี้เป็นตัวเลือกที่ดีที่สุดสำหรับนักพัฒนา
.substring(...)
รายการเสมอ
Java ใช้เพื่ออ้างถึงสตริงที่ใหญ่กว่า แต่:
ฉันรู้สึกว่ามันสามารถปรับปรุงได้แม้ว่า: ทำไมไม่ทำเพียงแค่การคัดลอกอย่างมีเงื่อนไข?
หากซับสตริงมีขนาดอย่างน้อยครึ่งหนึ่งของพาเรนต์หนึ่งสามารถอ้างอิงพาเรนต์ได้ มิฉะนั้นจะสามารถทำสำเนาได้ สิ่งนี้หลีกเลี่ยงการรั่วไหลของหน่วยความจำจำนวนมากในขณะที่ยังให้ประโยชน์ที่สำคัญ
char[]
(กับคำแนะนำที่แตกต่างกันที่จะเริ่มต้นและสิ้นสุด) String
เพื่อสร้างใหม่ String
นี้แสดงให้เห็นชัดเจนว่าการวิเคราะห์ต้นทุนและผลประโยชน์จะต้องแสดงการตั้งค่าสำหรับการสร้างใหม่
ไม่มีคำตอบที่ระบุที่นี่ "ปัญหาการถ่ายคร่อม" ซึ่งจะบอกว่าสตริงใน. NET จะแสดงเป็นชุดของ BStr (ความยาวที่เก็บไว้ในหน่วยความจำ "ก่อน" ตัวชี้) และ CStr (สตริงสิ้นสุดใน '\ 0')
ดังนั้นสตริง "Hello there" จึงถูกแทนด้วย
0B 00 00 00 48 00 65 00 6C 00 6F 00 20 00 74 00 68 00 65 00 72 00 65 00 00 00
(หากกำหนดให้ a char*
ใน - สถานะfixed
ตัวชี้จะชี้ไปที่ 0x48)
โครงสร้างนี้อนุญาตให้ค้นหาความยาวของสตริงได้อย่างรวดเร็ว (มีประโยชน์ในบริบทจำนวนมาก) และอนุญาตให้ตัวชี้ถูกส่งผ่านใน P / Invoke to Win32 (หรืออื่น ๆ ) APIs ซึ่งคาดว่าสตริงที่สิ้นสุดด้วยค่า null
เมื่อคุณทำ Substring(0, 5)
"โอ้ แต่ฉันสัญญาว่าจะมีกฎเป็นโมฆะหลังจากตัวละครตัวสุดท้าย" บอกว่าคุณต้องทำสำเนา แม้ว่าคุณจะได้ซับสตริงที่ส่วนท้ายแล้วก็ไม่มีที่ใดที่จะวางความยาวโดยไม่ทำให้ตัวแปรอื่นเสียหาย
อย่างไรก็ตามบางครั้งคุณต้องการพูดคุยเกี่ยวกับ "กึ่งกลางของสตริง" และคุณไม่จำเป็นต้องสนใจพฤติกรรม P / Invoke ReadOnlySpan<T>
โครงสร้างที่เพิ่งเพิ่มสามารถใช้เพื่อรับสตริงย่อยที่ไม่มีการคัดลอก:
string s = "Hello there";
ReadOnlySpan<char> hello = s.AsSpan(0, 5);
ReadOnlySpan<char> ell = hello.Slice(1, 3);
ReadOnlySpan<char>
"substring" ร้านค้าที่มีความยาวเป็นอิสระและมันก็ไม่ได้รับประกันว่ามี '\ 0' หลังจากการสิ้นสุดของค่า มันสามารถใช้งานได้หลายวิธี "เหมือนสตริง" แต่ไม่ใช่ "สตริง" เนื่องจากไม่มีคุณสมบัติ BStr หรือ CStr (ทั้งสองอย่างน้อยกว่า) หากคุณไม่เคย (โดยตรง) P / เรียกใช้มีความแตกต่างไม่มากนัก (ยกเว้นว่า API ที่คุณต้องการโทรไม่มีReadOnlySpan<char>
ภาระงานมากเกินไป)
ReadOnlySpan<char>
ไม่สามารถนำมาใช้เป็นข้อมูลอ้างอิงจากประเภทนี้ดังนั้นยังมีReadOnlyMemory<char>
( s.AsMemory(0, 5)
) ซึ่งเป็นทางอ้อมของการมีReadOnlySpan<char>
ดังนั้นเดียวกันแตกต่าง-from- string
มีอยู่
บางคำตอบ / ความคิดเห็นเกี่ยวกับคำตอบก่อนหน้านี้พูดคุยกันว่าการสิ้นเปลืองเพื่อให้ตัวเก็บขยะต้องเก็บสตริงล้านอักขระไว้รอบในขณะที่คุณพูดต่อไปประมาณ 5 ตัวอักษร นั่นคือพฤติกรรมที่คุณจะได้รับเมื่อReadOnlySpan<char>
เข้าใกล้ หากคุณเป็นเพียงการคำนวณสั้น ๆ แนวทาง ReadOnlySpan น่าจะดีกว่า หากคุณต้องการคงอยู่ชั่วขณะหนึ่งและคุณจะเก็บเพียงเล็กน้อยของสตริงเดิมการทำสตริงย่อยที่เหมาะสม (เพื่อตัดข้อมูลส่วนเกิน) น่าจะดีกว่า มีจุดเปลี่ยนอยู่ตรงกลาง แต่ขึ้นอยู่กับการใช้งานเฉพาะของคุณ