22 มีนาคม 2567
ในบทความนี้ เราจะสำรวจว่านักแปลของเราแปลงสมาชิกคลาส ตัวแปร ฟิลด์ ตัวดำเนินการ และโครงสร้างการควบคุม C# อย่างไร นอกจากนี้เรายังจะกล่าวถึงการใช้ไลบรารีสนับสนุนนักแปลสำหรับการแปลงประเภท .NET Framework เป็น C++ อย่างถูกต้อง
วิธีการเรียนแมปโดยตรงบน C ++ นอกจากนี้ยังใช้กับวิธีการแบบคงที่และตัวสร้างด้วย ในบางกรณี รหัสเพิ่มเติมอาจปรากฏขึ้น ตัวอย่างเช่น เพื่อจำลองการเรียกไปยังตัวสร้างแบบคงที่ วิธีการขยายและตัวดำเนินการจะถูกแปลเป็นวิธีการแบบคงที่และถูกเรียกอย่างชัดเจน ผู้เข้ารอบสุดท้ายกลายเป็นผู้ทำลายล้าง
ฟิลด์อินสแตนซ์ C# กลายเป็นฟิลด์อินสแตนซ์ C++ ฟิลด์แบบคงที่ยังคงไม่เปลี่ยนแปลง ยกเว้นในกรณีที่ลำดับการเริ่มต้นมีความสำคัญ – การดำเนินการนี้จะดำเนินการโดยการแปลฟิลด์ดังกล่าวเป็นซิงเกิลตัน
คุณสมบัติจะถูกแบ่งออกเป็นเมธอด getter และเมธอด setter หรือเพียงวิธีเดียวหากไม่มีเมธอดที่สอง สำหรับคุณสมบัติอัตโนมัติ จะมีการเพิ่มช่องค่าส่วนตัวด้วย คุณสมบัติคงที่จะถูกแบ่งออกเป็น getter และ setter แบบคงที่ ตัวสร้างดัชนีได้รับการประมวลผลโดยใช้ตรรกะเดียวกัน
กิจกรรมจะได้รับการแปลเป็นช่องต่างๆ ซึ่งเป็นประเภทที่สอดคล้องกับความเชี่ยวชาญพิเศษที่จำเป็นของ System::Event
การแปลในรูปแบบของสามวิธี (add
, remove
และ invoke
) จะมีความถูกต้องมากกว่า และยิ่งกว่านั้น ยังสามารถรองรับเหตุการณ์เชิงนามธรรมและเสมือนได้ บางทีในอนาคตเราจะมาถึงโมเดลดังกล่าว แต่ในขณะนี้ตัวเลือกคลาส Event
ครอบคลุมความต้องการของเราอย่างครบถ้วน
ตัวอย่างต่อไปนี้แสดงให้เห็นถึงกฎข้างต้น:
public abstract class Generic<T>
{
private T m_value;
public Generic(T value)
{
m_value = value;
}
~Generic()
{
m_value = default(T);
}
public string Property { get; set; }
public abstract int Property2 { get; }
public T this[int index]
{
get
{
return index == 0 ? m_value : default(T);
}
set
{
if (index == 0)
m_value = value;
else
throw new ArgumentException();
}
}
public event Action<int, int> IntIntEvent;
}
ผลการแปล C++ (ลบโค้ดที่ไม่มีนัยสำคัญออก):
template<typename T>
class Generic : public System::Object
{
public:
System::String get_Property()
{
return pr_Property;
}
void set_Property(System::String value)
{
pr_Property = value;
}
virtual int32_t get_Property2() = 0;
Generic(T value) : m_value(T())
{
m_value = value;
}
T idx_get(int32_t index)
{
return index == 0 ? m_value : System::Default<T>();
}
void idx_set(int32_t index, T value)
{
if (index == 0)
{
m_value = value;
}
else
{
throw System::ArgumentException();
}
}
System::Event<void(int32_t, int32_t)> IntIntEvent;
virtual ~Generic()
{
m_value = System::Default<T>();
}
private:
T m_value;
System::String pr_Property;
};
ฟิลด์คงที่และคงที่จะถูกแปลเป็นฟิลด์คงที่ ค่าคงที่คงที่ (ในบางกรณี – constexpr
) หรือเป็นวิธีการคงที่ที่ให้การเข้าถึงซิงเกิลตัน ฟิลด์อินสแตนซ์ C# จะถูกแปลงเป็นฟิลด์อินสแตนซ์ C++ ตัวเริ่มต้นที่ซับซ้อนใดๆ จะถูกย้ายไปยังตัวสร้าง และบางครั้งจำเป็นต้องเพิ่มตัวสร้างเริ่มต้นอย่างชัดเจนโดยที่ไม่มีอยู่ใน C# ตัวแปรสแต็กจะถูกส่งต่อไปตามที่เป็นอยู่ อาร์กิวเมนต์ของเมธอดจะถูกส่งต่อไปตามที่เป็นอยู่ ยกเว้นว่าทั้งอาร์กิวเมนต์ ref
และ out
จะกลายเป็นการอ้างอิง (โชคดีที่ห้ามไม่ให้มีการโหลดมากเกินไป)
ประเภทของฟิลด์และตัวแปรจะถูกแทนที่ด้วยค่าเทียบเท่า C++ ในกรณีส่วนใหญ่ สิ่งที่เทียบเท่ากันดังกล่าวจะถูกสร้างขึ้นโดยนักแปลเองจากซอร์สโค้ด C# ประเภทไลบรารี รวมถึงประเภท .NET Framework และอื่นๆ เขียนโดยเราในภาษา C++ และเป็นส่วนหนึ่งของไลบรารีสนับสนุนนักแปล ซึ่งมาพร้อมกับผลิตภัณฑ์ที่แปลงแล้ว var
ถูกแปลเป็น auto
ยกเว้นในกรณีที่จำเป็นต้องมีการระบุประเภทที่ชัดเจนเพื่อทำให้ความแตกต่างในการทำงานราบรื่นขึ้น
นอกจากนี้ ประเภทการอ้างอิงจะรวมอยู่ใน SmartPtr
ประเภทค่าจะถูกทดแทนตามที่เป็นอยู่ เนื่องจากอาร์กิวเมนต์ประเภทสามารถเป็นได้ทั้งประเภทค่าหรือประเภทการอ้างอิง จึงถูกแทนที่เช่นกัน แต่เมื่อสร้างอินสแตนซ์ อาร์กิวเมนต์อ้างอิงจะถูกรวมไว้ใน SharedPtr
ดังนั้น List<int>
จึงถูกแปลเป็น List<int32_t>
แต่ List<Object>
จะกลายเป็น List<SmartPtr<Object>>
ในบางกรณีพิเศษ ประเภทการอ้างอิงจะถูกแปลเป็นประเภทค่า ตัวอย่างเช่น การใช้งาน System::String
ของเรานั้นอิงตามประเภท UnicodeString
จาก ICU และได้รับการปรับให้เหมาะสมสำหรับพื้นที่จัดเก็บสแต็ก
เพื่อให้เห็นภาพ ลองแปลคลาสต่อไปนี้:
public class Variables
{
public int m_int;
private string m_string = new StringBuilder().Append("foobazz").ToString();
private Regex m_regex = new Regex("foo|bar");
public object Foo(int a, out int b)
{
b = a + m_int;
return m_regex.Match(m_string);
}
}
หลังจากแปลแล้ว จะใช้แบบฟอร์มต่อไปนี้ (ลบโค้ดที่ไม่มีนัยสำคัญออก):
class Variables : public System::Object
{
public:
int32_t m_int;
System::SharedPtr<System::Object> Foo(int32_t a, int32_t& b);
Variables();
private:
System::String m_string;
System::SharedPtr<System::Text::RegularExpressions::Regex> m_regex;
};
System::SharedPtr<System::Object> Variables::Foo(int32_t a, int32_t& b)
{
b = a + m_int;
return m_regex->Match(m_string);
}
Variables::Variables()
: m_int(0)
, m_regex(System::MakeObject<System::Text::RegularExpressions::Regex>(u"foo|bar"))
{
this->m_string = System::MakeObject<System::Text::StringBuilder>()->
Append(u"foobazz")->ToString();
}
ความคล้ายคลึงกันของโครงสร้างการควบคุมหลักส่งผลต่อมือของเรา ตัวดำเนินการเช่น if
, else
, switch
, while
, do
-while
, for
, try
-catch
, return
, break
และ continue
ส่วนใหญ่จะถูกถ่ายโอนตามที่เป็นอยู่ ข้อยกเว้นในรายการนี้อาจเป็นเพียง switch
เท่านั้น ซึ่งต้องได้รับการดูแลเป็นพิเศษบางประการ ประการแรก C# อนุญาตให้ใช้กับประเภทสตริง – ในภาษา C++ เราสร้างลำดับของ if
-else if
ในกรณีนี้ ประการที่สอง ความสามารถที่เพิ่มขึ้นเมื่อไม่นานมานี้ในการจับคู่นิพจน์ที่เลือกกับเทมเพลตประเภท ซึ่งยังสามารถขยายเป็นลำดับของ if
ได้อย่างง่ายดาย
โครงสร้างที่ไม่มีอยู่ใน C++ ถือเป็นที่สนใจ ดังนั้น ตัวดำเนินการ using
จะรับประกันการเรียกใช้เมธอด Dispose()
เมื่อออกจากบริบท ในภาษา C++ เราจำลองพฤติกรรมนี้โดยการสร้างอ็อบเจ็กต์ป้องกันบนสแต็ก ซึ่งจะเรียกเมธอดที่ต้องการใน destructor อย่างไรก็ตาม ก่อนหน้านั้น จำเป็นต้องจับข้อยกเว้นที่เกิดขึ้นโดยโค้ดซึ่งเป็นส่วนเนื้อความของ using
และเก็บ exception_ptr
ไว้ในฟิลด์ของตัวป้องกัน – หาก Dispose()
ไม่ทำให้เกิดข้อยกเว้น สิ่งที่เราเก็บไว้จะถูกโยนทิ้งใหม่ นี่เป็นเพียงกรณีที่ไม่ค่อยพบนักเมื่อการโยนข้อยกเว้นจาก destructor นั้นสมเหตุสมผลและไม่ใช่ข้อผิดพลาด บล็อก finally
ได้รับการแปลตามรูปแบบที่คล้ายกัน แทนที่จะใช้เมธอด Dispose()
เท่านั้น ฟังก์ชัน lambda จะถูกเรียก โดยที่นักแปลพันตัวของมันไว้
โอเปอเรเตอร์อื่นที่ไม่มีอยู่ใน C# และที่เราถูกบังคับให้จำลองคือ foreach
ในตอนแรก เราแปลเป็นภาษา while
ที่เทียบเท่ากัน โดยเรียกเมธอด MoveNext()
ของตัวแจงนับ ซึ่งเป็นวิธีสากลแต่ค่อนข้างช้า เนื่องจากการใช้งาน C++ ของคอนเทนเนอร์ .NET ส่วนใหญ่ใช้โครงสร้างข้อมูล STL เราจึงใช้ตัววนซ้ำดั้งเดิมเมื่อเป็นไปได้ โดยแปลง foreach
เป็น for
แบบอิงช่วง ในกรณีที่ไม่มีตัววนซ้ำดั้งเดิม (เช่น มีการใช้คอนเทนเนอร์ใน C# ล้วนๆ) จะใช้ตัววนซ้ำแบบ wrapper ซึ่งทำงานร่วมกับตัวแจงนับภายใน ก่อนหน้านี้ การเลือกวิธีการวนซ้ำที่ถูกต้องเป็นความรับผิดชอบของฟังก์ชันภายนอกที่เขียนโดยใช้เทคนิค SFINAE ตอนนี้เราใกล้จะมีเวอร์ชันที่ถูกต้องของเมธอด begin
-end
ในคอนเทนเนอร์ทั้งหมด รวมถึงเวอร์ชันที่แปลแล้วด้วย
เช่นเดียวกับโครงสร้างการควบคุม ตัวดำเนินการส่วนใหญ่ (อย่างน้อยก็ทางคณิตศาสตร์ ตรรกะ และการมอบหมาย) ไม่ต้องการการประมวลผลพิเศษ อย่างไรก็ตาม มีประเด็นที่ละเอียดอ่อน: ใน C# ลำดับการประเมินส่วนของนิพจน์นั้นถูกกำหนดไว้ ในขณะที่ใน C++ อาจมีพฤติกรรมที่ไม่ได้กำหนดไว้ในบางกรณี ตัวอย่างเช่น โค้ดที่แปลต่อไปนี้ทำงานแตกต่างออกไปหลังจากการคอมไพล์ด้วยเครื่องมือที่แตกต่างกัน:
auto offset32 = block[i++] + block[i++] * 256 + block[i++] * 256 * 256 +
block[i++] * 256 * 256 * 256;
โชคดีที่ปัญหาดังกล่าวค่อนข้างหายาก เรามีแผนที่จะสอนนักแปลให้จัดการกับช่วงเวลาดังกล่าว แต่เนื่องจากความซับซ้อนของการวิเคราะห์ที่ระบุสำนวนที่มีผลข้างเคียง จึงยังไม่ได้ดำเนินการ
อย่างไรก็ตาม แม้แต่ตัวดำเนินการที่ง่ายที่สุดก็ยังต้องมีการประมวลผลพิเศษเมื่อนำไปใช้กับคุณสมบัติ ดังที่แสดงไว้ข้างต้น คุณสมบัติจะถูกแบ่งออกเป็น getters และ setters และนักแปลจะต้องแทรกการเรียกที่จำเป็น ขึ้นอยู่กับบริบท:
obj1.Property = obj2.Property;
string s = GetObj().Property += "suffix";
obj1->set_Property(obj2->get_Property());
System::String s = System::setter_add_wrap(static_cast<MyClass*>(GetObj().GetPointer()),
&MyClass::get_Property, &MyClass::set_Property, u"suffix")
ในบรรทัดแรกการเปลี่ยนกลายเป็นเรื่องเล็กน้อย ประการที่สอง จำเป็นต้องใช้ wrapper setter_add_wrap
เพื่อให้แน่ใจว่าฟังก์ชัน GetObj()
ได้รับการเรียกเพียงครั้งเดียว และผลลัพธ์ของการเชื่อมการเรียก get_Property()
และสตริงลิเทอรัลจะถูกส่งผ่านไม่เพียงแต่ ไปยังเมธอด set_Property()
(ซึ่งส่งคืน void
) แต่ยังเพิ่มเติมเพื่อใช้ในนิพจน์ด้วย ใช้แนวทางเดียวกันนี้เมื่อเข้าถึงตัวสร้างดัชนี
ตัวดำเนินการ C# ที่ไม่ได้อยู่ใน C++: as
, is
, typeof
, default
, ??
, ?.
และอื่นๆ ได้รับการจำลองโดยใช้ฟังก์ชันไลบรารีการสนับสนุนนักแปล ในกรณีที่จำเป็นต้องหลีกเลี่ยงการประเมินอาร์กิวเมนต์ซ้ำซ้อน เช่น เพื่อไม่ให้เปิดเผย GetObj()?.Invoke()
ลงใน GetObj() ? GetObj().Invoke() : nullptr
ซึ่งเป็นแนวทางที่คล้ายกับ ใช้อันที่แสดงไว้ด้านบน
ตัวดำเนินการเข้าถึงสมาชิก (.
) อาจถูกแทนที่ด้วยสิ่งที่เทียบเท่าจาก C++ ขึ้นอยู่กับบริบท: ตัวดำเนินการแก้ไขขอบเขต (::
) หรือ “ลูกศร” (->
) ไม่จำเป็นต้องเปลี่ยนดังกล่าวเมื่อเข้าถึงสมาชิกของโครงสร้าง