ทำไมเราต้องใช้การชกมวยและการเลิกทำกล่องใน C #
ฉันรู้ว่ามวยและ unboxing คืออะไร แต่ฉันไม่สามารถเข้าใจการใช้งานจริงของมัน ทำไมและที่ไหนฉันจึงควรใช้
short s = 25;
object objshort = s; //Boxing
short anothershort = (short)objshort; //Unboxing
ทำไมเราต้องใช้การชกมวยและการเลิกทำกล่องใน C #
ฉันรู้ว่ามวยและ unboxing คืออะไร แต่ฉันไม่สามารถเข้าใจการใช้งานจริงของมัน ทำไมและที่ไหนฉันจึงควรใช้
short s = 25;
object objshort = s; //Boxing
short anothershort = (short)objshort; //Unboxing
คำตอบ:
ทำไม
การมีระบบชนิดรวมเป็นหนึ่งและอนุญาตให้ประเภทค่ามีการแสดงข้อมูลพื้นฐานที่แตกต่างกันอย่างสิ้นเชิงจากวิธีที่ประเภทการอ้างอิงเป็นตัวแทนข้อมูลพื้นฐาน (เช่นint
เป็นเพียงกลุ่มของบิตสามสิบสองซึ่งแตกต่างจากการอ้างอิงโดยสิ้นเชิง พิมพ์)
คิดว่ามันเป็นอย่างนี้ คุณมีตัวแปรประเภทo
object
และตอนนี้คุณมีและคุณต้องการที่จะใส่ลงในint
คือการอ้างอิงถึงบางสิ่งบางอย่างและที่สำคัญไม่ได้อ้างอิงถึงบางสิ่งบางอย่าง (หลังจากทั้งหมดมันเป็นเพียงตัวเลข) ดังนั้นสิ่งที่คุณทำคือ: คุณสร้างใหม่ที่สามารถเก็บและจากนั้นคุณกำหนดการอ้างอิงไปยังวัตถุนั้น เราเรียกกระบวนการนี้ว่า "มวย"o
o
int
object
int
o
ดังนั้นหากคุณไม่สนใจที่จะมีระบบแบบครบวงจร (เช่นประเภทการอ้างอิงและประเภทค่ามีการแสดงออกที่แตกต่างกันมากและคุณไม่ต้องการวิธีการทั่วไปในการ "แสดง" ทั้งสอง) ดังนั้นคุณไม่จำเป็นต้องใช้มวย หากคุณไม่สนใจเกี่ยวกับการint
แสดงค่าพื้นฐานของพวกเขา (เช่นแทนที่จะมีint
ประเภทอ้างอิงด้วยและเพียงแค่เก็บการอ้างอิงถึงมูลค่าพื้นฐานของพวกเขา) คุณไม่จำเป็นต้องชกมวย
ฉันควรใช้ที่ไหน
ตัวอย่างเช่นประเภทคอลเลกชันเก่าArrayList
กินobject
s เท่านั้น นั่นคือมันเก็บเฉพาะการอ้างอิงถึงสิ่งที่อาศัยอยู่ที่ไหนสักแห่ง หากไม่มีมวยคุณไม่สามารถใส่ชุดint
ดังกล่าวได้ แต่ด้วยการชกมวยคุณสามารถ
ตอนนี้ในยุคของนายพลคุณไม่ต้องการสิ่งนี้จริงๆและโดยทั่วไปสามารถไปอย่างสนุกสนานโดยไม่ต้องคิดเกี่ยวกับปัญหา แต่มีข้อควรระวังเล็กน้อยที่ควรระวัง:
สิ่งนี้ถูกต้อง:
double e = 2.718281828459045;
int ee = (int)e;
มันไม่ใช่:
double e = 2.718281828459045;
object o = e; // box
int ee = (int)o; // runtime exception
แต่คุณต้องทำสิ่งนี้:
double e = 2.718281828459045;
object o = e; // box
int ee = (int)(double)o;
ครั้งแรกที่เราได้อย่างชัดเจน unbox double
( (double)o
) int
แล้วโยนไปยังที่
ผลลัพธ์ของสิ่งต่อไปนี้คืออะไร:
double e = 2.718281828459045;
double d = e;
object o1 = d;
object o2 = e;
Console.WriteLine(d == e);
Console.WriteLine(o1 == o2);
ลองคิดดูสักครู่ก่อนจะไปยังประโยคถัดไป
ถ้าคุณพูดTrue
และFalse
ยอดเยี่ยม! รออะไร? นั่นเป็นเพราะ==
ประเภทการอ้างอิงใช้การอ้างอิงความเท่าเทียมกันซึ่งจะตรวจสอบว่าการอ้างอิงมีความเท่าเทียมกันหรือไม่ถ้าค่าอ้างอิงนั้นเท่ากัน นี่เป็นข้อผิดพลาดที่ง่ายที่จะทำอันตราย บางทีอาจจะลึกซึ้งยิ่งขึ้น
double e = 2.718281828459045;
object o1 = e;
object o2 = e;
Console.WriteLine(o1 == o2);
จะพิมพ์False
!
ดีกว่าที่จะพูดว่า:
Console.WriteLine(o1.Equals(o2));
ซึ่งจะแล้วขอบคุณมาก ๆ True
พิมพ์
หนึ่งความละเอียดสุดท้าย:
[struct|class] Point {
public int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
Point p = new Point(1, 1);
object o = p;
p.x = 2;
Console.WriteLine(((Point)o).x);
ผลลัพธ์คืออะไร มันขึ้นอยู่กับ! หากPoint
เป็นเช่นstruct
นั้นผลลัพธ์จะเป็น1
แต่ถ้าPoint
เป็นเช่นclass
นั้นผลลัพธ์จะเป็น2
! การแปลงมวยทำสำเนาของค่าที่บรรจุอยู่เพื่ออธิบายความแตกต่างของพฤติกรรม
boxing
และunboxing
?
ในกรอบงาน. NET มีชนิดของสองชนิดคือชนิดของค่าและชนิดของการอ้างอิง นี่เป็นเรื่องธรรมดาในภาษา OO
หนึ่งในคุณสมบัติที่สำคัญของภาษาเชิงวัตถุคือความสามารถในการจัดการอินสแตนซ์ในลักษณะที่ไม่เชื่อเรื่องพระเจ้า นี้จะเรียกว่าเป็นความแตกต่าง เนื่องจากเราต้องการใช้ประโยชน์จากความหลากหลาย แต่เรามีสองชนิดที่แตกต่างกันจึงต้องมีวิธีที่จะนำมันมารวมกันเพื่อให้เราสามารถจัดการอย่างใดอย่างหนึ่ง
ตอนนี้ย้อนกลับไปในสมัยก่อน (1.0 ของ Microsoft.NET) ไม่มีฮัลลาบุลทั่วไปชื่อใหม่ คุณไม่สามารถเขียนวิธีที่มีอาร์กิวเมนต์เดียวที่สามารถให้บริการประเภทค่าและประเภทการอ้างอิงได้ นั่นเป็นการละเมิดความหลากหลาย ดังนั้นมวยถูกนำมาใช้เป็นวิธีการบังคับประเภทของค่าลงในวัตถุ
หากไม่สามารถทำได้กรอบจะเกลื่อนไปด้วยวิธีการและชั้นเรียนที่มีจุดประสงค์เพียงเพื่อรับสายพันธุ์อื่น ๆ ไม่เพียงแค่นั้น แต่เนื่องจากประเภทค่าไม่ได้แชร์บรรพบุรุษประเภททั่วไปอย่างแท้จริงคุณจะต้องมีวิธีการโอเวอร์โหลดที่แตกต่างกันสำหรับแต่ละประเภทค่า (บิต, ไบต์, int16, int32 ฯลฯ ฯลฯ )
ชกมวยป้องกันไม่ให้สิ่งนี้เกิดขึ้น และนั่นเป็นเหตุผลว่าทำไมอังกฤษจึงฉลองวัน Boxing Day
List<string>.Enumerator
จะให้IEnumerator<string>
ผลกับวัตถุซึ่งส่วนใหญ่ทำตัวเหมือนคลาสประเภท แต่ด้วยEquals
วิธีการที่แตก วิธีที่ดีกว่าในการส่งList<string>.Enumerator
ต่อไปที่IEnumerator<string>
จะเรียกใช้ตัวดำเนินการแปลงที่กำหนดเอง แต่การมีอยู่ของการแปลงโดยนัยจะป้องกันไม่ให้
วิธีที่ดีที่สุดที่จะเข้าใจสิ่งนี้คือการดูภาษาการเขียนโปรแกรมระดับล่าง C # ประกอบ
ในภาษาระดับต่ำสุดเช่น C ตัวแปรทั้งหมดมีที่เดียว: The Stack ทุกครั้งที่คุณประกาศตัวแปรมันจะไปที่สแต็ก พวกเขาสามารถเป็นค่าดั้งเดิมเช่นบูล, ไบต์, int 32- บิต, uint 32 บิต ฯลฯ สแต็คมีทั้งง่ายและรวดเร็ว เมื่อตัวแปรถูกเพิ่มเข้าไปพวกมันจะอยู่ข้างบนอีกอันหนึ่งดังนั้นอันดับแรกที่คุณประกาศจะอยู่ที่ 0x00, ถัดไปที่ 0x01, ถัดไปที่ 0x02 ใน RAM, ฯลฯ นอกจากนี้ตัวแปรมักจะถูกแก้ไขล่วงหน้าที่คอมไพล์ - เวลาดังนั้นที่อยู่ของพวกเขาจึงเป็นที่รู้จักกันก่อนที่คุณจะรันโปรแกรม
ในระดับต่อไปเช่น C ++ โครงสร้างหน่วยความจำที่สองที่เรียกว่า Heap ถูกนำเสนอ คุณยังคงอาศัยอยู่ในสแต็กเป็นส่วนใหญ่ แต่ ints พิเศษที่เรียกว่าพอยน์เตอร์สามารถเพิ่มลงในสแต็คที่เก็บที่อยู่หน่วยความจำสำหรับไบต์แรกของวัตถุและวัตถุนั้นอาศัยอยู่ในฮีป Heap นั้นค่อนข้างยุ่งเหยิงและค่อนข้างแพงในการบำรุงรักษาเพราะแตกต่างจากตัวแปร Stack พวกมันไม่กองซ้อนกันเป็นเส้นตรงและลงไปตามโปรแกรมที่เรียกใช้งาน พวกเขาสามารถมาและไปในลำดับไม่เฉพาะและพวกเขาสามารถเติบโตและหดตัว
การรับมือกับพอยน์เตอร์นั้นยาก สาเหตุของการรั่วไหลของหน่วยความจำบัฟเฟอร์โอเวอร์รันและความยุ่งยาก C # เพื่อช่วยเหลือ
ในระดับที่สูงขึ้น C # คุณไม่จำเป็นต้องคิดถึงพอยน์เตอร์ - กรอบงาน. Net (เขียนใน C ++) คิดเกี่ยวกับสิ่งเหล่านี้ให้คุณและนำเสนอให้คุณในรูปแบบ Reference to Objects และเพื่อประสิทธิภาพช่วยให้คุณเก็บค่าได้ง่ายขึ้น เช่น bools, bytes และ ints เป็นประเภทค่า ภายใต้ประทุนวัตถุและสิ่งต่าง ๆ ที่ยกระดับคลาสไปสู่ Heap ที่มีราคาแพง Memory-Managed Heap ในขณะที่ Value Type จะไปใน Stack เดียวกันกับที่คุณมีใน C ระดับต่ำ - เร็วมาก
เพื่อประโยชน์ในการรักษาปฏิสัมพันธ์ระหว่าง 2 แนวคิดพื้นฐานที่แตกต่างกันของหน่วยความจำ (และกลยุทธ์สำหรับการจัดเก็บ) ง่าย ๆ จากมุมมองของโคเดอร์ Boxing ทำให้ค่าที่จะคัดลอกมาจากสแต็คใส่ในวัตถุและวางบนกอง - แพงกว่า แต่ปฏิสัมพันธ์ของเหลวกับโลกอ้างอิง ดังที่คำตอบอื่น ๆ ชี้ให้เห็นสิ่งนี้จะเกิดขึ้นเมื่อคุณพูดเช่น:
bool b = false; // Cheap, on Stack
object o = b; // Legal, easy to code, but complex - Boxing!
bool b2 = (bool)o; // Unboxing!
ภาพประกอบที่ดีเกี่ยวกับข้อได้เปรียบของ Boxing คือการตรวจสอบ null:
if (b == null) // Will not compile - bools can't be null
if (o == null) // Will compile and always return false
อ็อบเจกต์ o ของเราคือที่อยู่ทางเทคนิคในสแต็กที่ชี้ไปที่สำเนาของบูล bool ของเราซึ่งถูกคัดลอกไปที่ฮีป เราสามารถตรวจสอบ o เป็นโมฆะได้เพราะบูลนั้นถูกวางไว้แล้ว
โดยทั่วไปคุณควรหลีกเลี่ยงการชกมวยเว้นแต่ว่าคุณต้องการมันตัวอย่างเช่นการส่ง int / bool / อะไรก็ตามที่เป็นวัตถุไปยังการโต้แย้ง มีโครงสร้างพื้นฐานบางอย่างใน. Net ที่ยังคงต้องการประเภทของค่าที่ส่งผ่านในฐานะวัตถุ (และต้องมีมวย) แต่ส่วนใหญ่คุณไม่จำเป็นต้องใช้กล่อง
รายการโครงสร้าง C # ในประวัติศาสตร์ที่ต้องใช้มวยซึ่งคุณควรหลีกเลี่ยง:
ระบบเหตุการณ์กลายเป็นสภาพการแข่งขันที่ไร้เดียงสาใช้และไม่สนับสนุน async เพิ่มในปัญหามวยและควรหลีกเลี่ยง (คุณสามารถแทนที่มันด้วยระบบเหตุการณ์ async ที่ใช้ Generics)
แบบจำลองเธรดและตัวจับเวลาแบบเก่าบังคับให้ใช้พารามิเตอร์กับกล่อง แต่ถูกแทนที่ด้วย async / คอยซึ่งมีความสะอาดกว่าและมีประสิทธิภาพมากกว่า
.Net 1.1 กลุ่มทั้งหมดอาศัยมวยเพราะพวกเขามาก่อน Generics เหล่านี้ยังคงเตะรอบใน System.Collections ในรหัสใหม่ใด ๆ ที่คุณควรใช้ Collections จาก System.Collections.Generic ซึ่งนอกเหนือจากการหลีกเลี่ยง Boxing ยังช่วยให้คุณปลอดภัยยิ่งขึ้นด้วย
คุณควรหลีกเลี่ยงการประกาศหรือส่งประเภทค่าของคุณเป็นวัตถุเว้นแต่คุณจะต้องจัดการกับปัญหาทางประวัติศาสตร์ข้างต้นที่บังคับให้มวยและคุณต้องการหลีกเลี่ยงการตีมวยที่มีประสิทธิภาพในภายหลังเมื่อคุณรู้ว่ามันจะถูกบรรจุอยู่แล้ว
ตามคำแนะนำของ Mikael ด้านล่าง:
using System.Collections.Generic;
var employeeCount = 5;
var list = new List<int>(10);
using System.Collections;
Int32 employeeCount = 5;
var list = new ArrayList(10);
คำตอบนี้เดิมแนะนำให้ใช้ Int32, Bool และอื่น ๆ ทำให้เกิดการชกมวยเมื่อในความเป็นจริงพวกเขาเป็นนามแฝงง่ายสำหรับประเภทค่า นั่นคือ. Net มีประเภทเช่น Bool, Int32, String และ C # เป็นนามแฝงเป็น bool, int, string โดยไม่มีความแตกต่างในการทำงาน
Boxing ไม่ใช่สิ่งที่คุณใช้จริงๆ - มันเป็นสิ่งที่ runtime ใช้เพื่อให้คุณสามารถจัดการกับชนิดของการอ้างอิงและค่าในแบบเดียวกันเมื่อจำเป็น ตัวอย่างเช่นถ้าคุณใช้ ArrayList เพื่อเก็บรายการจำนวนเต็มจำนวนเต็มจะได้รับการบรรจุให้พอดีกับสล็อตประเภทวัตถุใน ArrayList
ตอนนี้ใช้คอลเล็กชั่นทั่วไปหมดไปแล้ว หากคุณสร้าง a List<int>
จะไม่มีการชกมวย - List<int>
สามารถเก็บจำนวนเต็มได้โดยตรง
Boxing และ Unboxing ใช้เพื่อรักษาวัตถุประเภทค่าเป็นประเภทอ้างอิง ย้ายมูลค่าที่แท้จริงไปยังฮีปที่ได้รับการจัดการและเข้าถึงมูลค่าโดยอ้างอิง
หากไม่มีมวยและการแกะกล่องคุณจะไม่สามารถผ่านประเภทค่าโดยการอ้างอิง และนั่นหมายความว่าคุณไม่สามารถส่งค่าประเภทเป็นอินสแตนซ์ของ Object
สถานที่สุดท้ายที่ฉันต้อง unbox บางสิ่งบางอย่างคือเมื่อเขียนรหัสที่ดึงข้อมูลบางอย่างจากฐานข้อมูล (ฉันไม่ได้ใช้LINQ กับ SQLเพียงADO.NETเก่าธรรมดา):
int myIntValue = (int)reader["MyIntValue"];
โดยทั่วไปหากคุณทำงานกับ API ที่เก่ากว่าก่อนชื่อสามัญคุณจะพบกับมวย นอกเหนือจากนั้นมันไม่ธรรมดา
ต้องใช้ Boxing เมื่อเรามีฟังก์ชั่นที่ต้องการวัตถุเป็นพารามิเตอร์ แต่เรามีประเภทค่าต่าง ๆ ที่จำเป็นต้องผ่านในกรณีนี้เราต้องแปลงชนิดของค่าให้เป็นชนิดข้อมูลวัตถุก่อนส่งผ่านไปยังฟังก์ชัน
ฉันไม่คิดว่าเป็นจริงลองแทน:
class Program
{
static void Main(string[] args)
{
int x = 4;
test(x);
}
static void test(object o)
{
Console.WriteLine(o.ToString());
}
}
มันใช้งานได้ดีฉันไม่ได้ใช้มวย / unboxing (ถ้าคอมไพเลอร์ทำอย่างนั้นไม่ได้อยู่เบื้องหลัง)
ใน. net ทุกอินสแตนซ์ของ Object หรือชนิดใด ๆ ที่ได้มาจากนั้นรวมถึงโครงสร้างข้อมูลที่มีข้อมูลเกี่ยวกับประเภทของมัน ประเภทค่า "Real" ใน. net ไม่มีข้อมูลดังกล่าว ในการอนุญาตให้ข้อมูลในประเภทค่าถูกจัดการโดยรูทีนที่คาดว่าจะได้รับประเภทที่มาจากวัตถุระบบจะกำหนดประเภทค่าที่สอดคล้องกันให้กับแต่ละประเภทคลาสที่สอดคล้องกับสมาชิกและเขตข้อมูลเดียวกัน Boxing สร้างอินสแตนซ์ใหม่ของประเภทคลาสนี้คัดลอกฟิลด์จากอินสแตนซ์ประเภทค่า การแกะกล่องออกจะเป็นการคัดลอกฟิลด์จากอินสแตนซ์ของประเภทคลาสไปยังอินสแตนซ์ของประเภทค่า ประเภทคลาสทั้งหมดที่สร้างขึ้นจากประเภทค่ามาจากคลาส ValueType ที่มีชื่ออย่างไม่เจาะจง (ซึ่งถึงแม้ชื่อจะเป็นประเภทอ้างอิงจริง)
เมื่อเมธอดใช้ชนิดการอ้างอิงเป็นพารามิเตอร์เท่านั้น (กล่าวว่าเมธอดทั่วไปที่ถูก จำกัด ให้เป็นคลาสผ่านnew
ข้อ จำกัด ) คุณจะไม่สามารถส่งประเภทการอ้างอิงไปยังมันได้และจะต้องทำแบบนั้น
นี่เป็นจริงสำหรับวิธีการใด ๆ ที่ใช้object
เป็นพารามิเตอร์ - ซึ่งจะต้องเป็นประเภทการอ้างอิง
โดยทั่วไปแล้วคุณมักจะต้องการหลีกเลี่ยงการชกมวยประเภทค่าของคุณ
อย่างไรก็ตามมีเหตุการณ์ที่เกิดขึ้นได้ยากซึ่งสิ่งนี้มีประโยชน์ หากคุณต้องการกำหนดเป้าหมายเป็นเฟรมเวิร์ก 1.1 คุณจะไม่สามารถเข้าถึงคอลเล็กชันทั่วไปได้ การใช้คอลเลกชันใด ๆ ใน. NET 1.1 จะต้องดำเนินการกับประเภทค่าของคุณเป็น System.Object ซึ่งเป็นสาเหตุของการ Boxing / Unbox
ยังมีบางกรณีที่สิ่งนี้จะเป็นประโยชน์ใน. NET 2.0+ เมื่อใดก็ตามที่คุณต้องการใช้ประโยชน์จากความจริงที่ว่าทุกประเภทรวมถึงประเภทของมูลค่าสามารถได้รับการปฏิบัติเหมือนเป็นวัตถุโดยตรงคุณอาจต้องใช้ Boxing / unboxing บางครั้งสิ่งนี้มีประโยชน์เนื่องจากช่วยให้คุณสามารถบันทึกประเภทใดก็ได้ในคอลเล็กชัน (โดยใช้วัตถุแทน T ในคอลเล็กชันทั่วไป) แต่โดยทั่วไปจะดีกว่าเพื่อหลีกเลี่ยงสิ่งนี้เนื่องจากคุณสูญเสียความปลอดภัยประเภท ในบางกรณีที่การชกมวยเกิดขึ้นบ่อยครั้งคือเมื่อคุณใช้ Reflection การโทรที่ต้องไตร่ตรองจะต้องใช้การชกมวย / ไม่ทำกล่องเมื่อทำงานกับประเภทค่าเนื่องจากไม่ทราบชนิดล่วงหน้า
Boxing คือการแปลงค่าเป็นประเภทการอ้างอิงพร้อมกับข้อมูลที่ออฟเซ็ตบางอย่างในวัตถุบนฮีป
สำหรับสิ่งที่มวยทำจริง นี่คือตัวอย่างบางส่วน
โมโน C ++
void* mono_object_unbox (MonoObject *obj)
{
MONO_EXTERNAL_ONLY_GC_UNSAFE (void*, mono_object_unbox_internal (obj));
}
#define MONO_EXTERNAL_ONLY_GC_UNSAFE(t, expr) \
t result; \
MONO_ENTER_GC_UNSAFE; \
result = expr; \
MONO_EXIT_GC_UNSAFE; \
return result;
static inline gpointer
mono_object_get_data (MonoObject *o)
{
return (guint8*)o + MONO_ABI_SIZEOF (MonoObject);
}
#define MONO_ABI_SIZEOF(type) (MONO_STRUCT_SIZE (type))
#define MONO_STRUCT_SIZE(struct) MONO_SIZEOF_ ## struct
#define MONO_SIZEOF_MonoObject (2 * MONO_SIZEOF_gpointer)
typedef struct {
MonoVTable *vtable;
MonoThreadsSync *synchronisation;
} MonoObject;
Unboxing in Mono เป็นกระบวนการชี้พอยน์เตอร์ที่ออฟเซ็ต 2 gpointers ในวัตถุ (เช่น 16 ไบต์) เป็นgpointer
void*
สิ่งนี้สมเหตุสมผลเมื่อดูที่คำจำกัดความMonoObject
เนื่องจากเป็นเพียงส่วนหัวของข้อมูลอย่างชัดเจน
C ++
ในการใส่ค่าใน C ++ คุณสามารถทำสิ่งต่อไปนี้:
#include <iostream>
#define Object void*
template<class T> Object box(T j){
return new T(j);
}
template<class T> T unbox(Object j){
T temp = *(T*)j;
delete j;
return temp;
}
int main() {
int j=2;
Object o = box(j);
int k = unbox<int>(o);
std::cout << k;
}