Mengapa kita perlu tinju dan unboxing di C #?

325

Mengapa kita perlu tinju dan unboxing di C #?

Saya tahu apa itu tinju dan unboxing, tapi saya tidak bisa memahami penggunaannya yang sebenarnya. Mengapa dan di mana saya harus menggunakannya?

short s = 25;

object objshort = s;  //Boxing

short anothershort = (short)objshort;  //Unboxing
Vaibhav Jain
sumber

Jawaban:

482

Mengapa

Untuk memiliki sistem tipe terpadu dan memungkinkan tipe nilai memiliki representasi yang sama sekali berbeda dari data dasar mereka dari cara tipe referensi mewakili data dasar mereka (misalnya, inthanya seember tiga puluh dua bit yang benar-benar berbeda dari referensi Tipe).

Pikirkan seperti ini. Anda memiliki variabel otipe object. Dan sekarang Anda memiliki intdan ingin memasukkannya ke dalam o. oadalah referensi ke sesuatu di suatu tempat, dan dengan inttegas bukan referensi ke sesuatu di suatu tempat (setelah semua, itu hanya angka). Jadi, apa yang Anda lakukan adalah ini: Anda membuat yang baru objectyang dapat menyimpan intdan kemudian Anda menetapkan referensi ke objek itu o. Kami menyebut proses ini "tinju."

Jadi, jika Anda tidak peduli tentang memiliki sistem tipe terpadu (yaitu, tipe referensi dan tipe nilai memiliki representasi yang sangat berbeda dan Anda tidak ingin cara umum untuk "mewakili" keduanya) maka Anda tidak perlu bertinju. Jika Anda tidak peduli untuk intmewakili nilai dasarnya (yaitu, alih-alih intmenjadi tipe referensi juga dan simpan referensi ke nilai dasarnya) maka Anda tidak perlu bertinju.

di mana saya harus menggunakannya.

Misalnya, tipe koleksi lama ArrayListhanya memakan objects. Artinya, itu hanya menyimpan referensi ke sesuatu yang tinggal di suatu tempat. Tanpa tinju Anda tidak dapat memasukkan intkoleksi seperti itu. Tapi dengan tinju, kamu bisa.

Sekarang, di zaman generik Anda tidak benar-benar membutuhkan ini dan secara umum dapat berjalan dengan riang tanpa memikirkan masalahnya. Tetapi ada beberapa peringatan yang harus diperhatikan:

Ini benar:

double e = 2.718281828459045;
int ee = (int)e;

Ini bukan:

double e = 2.718281828459045;
object o = e; // box
int ee = (int)o; // runtime exception

Sebaliknya, Anda harus melakukan ini:

double e = 2.718281828459045;
object o = e; // box
int ee = (int)(double)o;

Pertama kita harus secara eksplisit menghapus kotaknya double ( (double)o) dan kemudian melemparkannya ke int.

Apa hasil dari hal berikut:

double e = 2.718281828459045;
double d = e;
object o1 = d;
object o2 = e;
Console.WriteLine(d == e);
Console.WriteLine(o1 == o2);

Pikirkan sejenak sebelum melanjutkan ke kalimat berikutnya.

Jika Anda mengatakan Truedan Falsehebat! Tunggu apa? Itu karena ==pada tipe referensi menggunakan referensi-kesetaraan yang memeriksa apakah referensi sama, bukan jika nilai yang mendasarinya sama. Ini adalah kesalahan yang sangat mudah dilakukan. Mungkin bahkan lebih halus

double e = 2.718281828459045;
object o1 = e;
object o2 = e;
Console.WriteLine(o1 == o2);

juga akan dicetak False !

Lebih baik dikatakan:

Console.WriteLine(o1.Equals(o2));

yang kemudian akan, untungnya, dicetak True .

Satu kehalusan terakhir:

[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);

Apa outputnya? Tergantung! Jika Pointa structmaka outputnya adalah 1tetapi jika Pointa classmaka outputnya adalah 2! Konversi tinju membuat salinan nilai yang sedang kotak menjelaskan perbedaan dalam perilaku.

jason
sumber
@Jason Apakah Anda bermaksud mengatakan bahwa jika kami memiliki daftar primitif, tidak ada alasan untuk menggunakan tinju / unboxing?
Pacerier
Saya tidak yakin apa yang Anda maksud dengan "daftar primitif."
jason
3
Bisakah Anda berbicara dengan dampak kinerja dari boxingdan unboxing?
Kevin Meredith
@KevinMeredith ada penjelasan dasar tentang kinerja untuk operasi tinju dan unboxing di msdn.microsoft.com/en-us/library/ms173196.aspx
InfZero
2
Jawaban yang sangat bagus - lebih baik daripada kebanyakan penjelasan yang pernah saya baca di buku-buku terkenal.
FredM
59

Dalam .NET framework, ada dua jenis tipe - tipe nilai dan tipe referensi. Ini relatif umum dalam bahasa OO.

Salah satu fitur penting dari bahasa berorientasi objek adalah kemampuan untuk menangani instance dengan cara tipe-agnostik. Ini disebut polimorfisme . Karena kami ingin mengambil keuntungan dari polimorfisme, tetapi kami memiliki dua jenis spesies yang berbeda, harus ada beberapa cara untuk menyatukan mereka sehingga kami dapat menangani satu atau yang lain dengan cara yang sama.

Sekarang, kembali ke masa lalu (1.0 dari Microsoft.NET), tidak ada hullabaloo generik bermodel baru ini. Anda tidak bisa menulis metode yang memiliki argumen tunggal yang bisa melayani tipe nilai dan tipe referensi. Itu pelanggaran polimorfisme. Jadi tinju diadopsi sebagai sarana untuk memaksa tipe nilai menjadi objek.

Jika ini tidak memungkinkan, kerangka kerja akan dipenuhi dengan metode dan kelas yang satu-satunya tujuan adalah untuk menerima spesies jenis lain. Tidak hanya itu, tetapi karena tipe nilai tidak benar-benar berbagi leluhur tipe yang sama, Anda harus memiliki kelebihan metode yang berbeda untuk setiap tipe nilai (bit, byte, int16, int32, dll, dll.).

Tinju mencegah hal ini terjadi. Dan itu sebabnya Inggris merayakan Boxing Day.


sumber
1
Sebelum obat generik, tinju otomatis diperlukan untuk melakukan banyak hal; mengingat keberadaan obat generik, jika bukan karena kebutuhan untuk menjaga kompatibilitas dengan kode lama, saya pikir .net akan lebih baik tanpa konversi tinju tersirat. Casting jenis nilai seperti List<string>.Enumeratoruntuk IEnumerator<string>menghasilkan sebuah objek yang sebagian besar berperilaku seperti jenis kelas, tetapi dengan patah Equalsmetode. Cara yang lebih baik untuk cor List<string>.Enumeratoruntuk IEnumerator<string>akan memanggil operator konversi kustom, tetapi keberadaan mencegah konversi tersirat itu.
supercat
42

Cara terbaik untuk memahami ini adalah dengan melihat bahasa pemrograman tingkat rendah yang dibangun oleh C #.

Dalam bahasa tingkat terendah seperti C, semua variabel masuk satu tempat: Stack. Setiap kali Anda mendeklarasikan variabel, variabel itu berada di Stack. Mereka hanya bisa berupa nilai primitif, seperti bool, byte, int 32-bit, uint 32-bit, dll. Stack sederhana dan cepat. Sebagai variabel ditambahkan mereka hanya pergi satu di atas yang lain, jadi yang pertama Anda mendeklarasikan duduk katakanlah, 0x00, berikutnya pada 0x01, berikutnya pada 0x02 dalam RAM, dll. Selain itu, variabel sering dialamatkan di compile- waktu, sehingga alamat mereka diketahui bahkan sebelum Anda menjalankan program.

Di tingkat berikutnya, seperti C ++, struktur memori kedua yang disebut Heap diperkenalkan. Anda sebagian besar masih tinggal di Stack, tetapi int khusus disebut Pointer dapat ditambahkan ke Stack, yang menyimpan alamat memori untuk byte pertama dari sebuah Object, dan Object tersebut tinggal di Heap. Heap agak berantakan dan agak mahal untuk dipertahankan, karena tidak seperti variabel Stack, mereka tidak menumpuk secara linear ke atas dan kemudian turun ketika program dijalankan. Mereka dapat datang dan pergi tanpa urutan tertentu, dan mereka dapat tumbuh dan menyusut.

Berurusan dengan pointer sulit. Mereka adalah penyebab kebocoran memori, buffer overruns, dan frustrasi. C # untuk menyelamatkan.

Pada level yang lebih tinggi, C #, Anda tidak perlu memikirkan pointer - kerangka .Net (ditulis dalam C ++) memikirkan ini untuk Anda dan menyajikannya kepada Anda sebagai Referensi untuk Objek, dan untuk kinerja, memungkinkan Anda menyimpan nilai yang lebih sederhana seperti bools, bytes, dan ints sebagai Value Type. Di bawah kap, Obyek dan barang-barang yang instantiates Kelas pergi pada Heap, mahal-Managed Heap, sementara Tipe Nilai pergi di tumpukan yang sama yang Anda miliki di C tingkat rendah - super cepat.

Demi menjaga interaksi antara 2 konsep memori yang berbeda (dan strategi penyimpanan) ini secara sederhana dari perspektif pembuat kode, Jenis Nilai dapat Dikemas kapan saja. Boxing menyebabkan nilai yang akan disalin dari Stack, dimasukkan ke dalam Object, dan ditempatkan di Heap - lebih mahal, tetapi, interaksi yang lancar dengan dunia Reference. Seperti jawaban lain tunjukkan, ini akan terjadi ketika Anda misalnya mengatakan:

bool b = false; // Cheap, on Stack
object o = b; // Legal, easy to code, but complex - Boxing!
bool b2 = (bool)o; // Unboxing!

Ilustrasi kuat tentang keuntungan Boxing adalah cek untuk nol:

if (b == null) // Will not compile - bools can't be null
if (o == null) // Will compile and always return false

Objek kami secara teknis adalah alamat di Stack yang menunjuk ke salinan bool b kami, yang telah disalin ke Heap. Kita dapat mengecek o untuk null karena bool telah di-Box dan diletakkan di sana.

Secara umum Anda harus menghindari Boxing kecuali Anda membutuhkannya, misalnya untuk melewatkan int / bool / apa pun sebagai objek argumen. Ada beberapa struktur dasar di. Net yang masih menuntut melewati Jenis Nilai sebagai objek (dan karenanya memerlukan Boxing), tetapi untuk sebagian besar Anda tidak perlu Box.

Daftar struktur C # historis yang tidak lengkap yang membutuhkan Boxing, yang harus Anda hindari:

  • Sistem Acara ternyata memiliki Kondisi Balap dalam penggunaan naif itu, dan itu tidak mendukung async. Tambahkan masalah Tinju dan mungkin harus dihindari. (Anda bisa menggantinya misalnya dengan sistem acara async yang menggunakan Generics.)

  • Model Threading dan Timer lama memaksa Box pada parameter mereka tetapi telah digantikan oleh async / wait yang jauh lebih bersih dan lebih efisien.

  • Koleksi .Net 1.1 sepenuhnya mengandalkan Boxing, karena mereka datang sebelum Generics. Ini masih menendang di System.Collections. Dalam kode baru apa pun Anda harus menggunakan Koleksi dari System.Collections.Generic, yang selain menghindari Tinju juga memberi Anda keamanan jenis yang lebih kuat .

Anda harus menghindari menyatakan atau melewati Jenis Nilai Anda sebagai objek, kecuali jika Anda harus berurusan dengan masalah historis di atas yang memaksa Boxing, dan Anda ingin menghindari hit kinerja Boxing itu nanti ketika Anda tahu itu akan menjadi Boxed pula.

Saran Per Mikael di bawah ini:

Melakukan hal ini

using System.Collections.Generic;

var employeeCount = 5;
var list = new List<int>(10);

Bukan ini

using System.Collections;

Int32 employeeCount = 5;
var list = new ArrayList(10);

Memperbarui

Jawaban ini awalnya menyarankan Int32, Bool dll menyebabkan tinju, padahal sebenarnya itu adalah alias sederhana untuk Jenis Nilai. Artinya, .Net memiliki tipe seperti Bool, Int32, String, dan C # alias untuk bool, int, string, tanpa perbedaan fungsional.

Chris Moschini
sumber
4
Anda mengajari saya apa yang tidak bisa dijelaskan oleh seratus programmer dan profesional TI dalam bertahun-tahun, tetapi ubah untuk mengatakan apa yang harus Anda lakukan alih-alih apa yang harus dihindari, karena itu agak sulit untuk diikuti .. aturan dasar yang paling sering tidak berjalan 1 Anda tidak boleh melakukan ini, melainkan lakukan ini
Mikael Puusaari
2
Jawaban ini seharusnya ditandai sebagai JAWABAN seratus kali!
Pouyan
3
tidak ada "Int" di c #, ada int dan Int32. Saya percaya Anda salah dalam menyatakan satu adalah tipe nilai dan yang lainnya adalah tipe referensi yang membungkus tipe nilai. kecuali saya salah, itu benar di Jawa, tetapi tidak C #. Dalam C # yang muncul biru di IDE adalah alias untuk definisi struct mereka. Jadi: int = Int32, bool = Boolean, string = String. Alasan untuk menggunakan bool di atas Boolean adalah karena disarankan demikian dalam pedoman desain dan konvensi MSDN. Kalau tidak, saya suka jawaban ini. Tapi saya akan memilih sampai Anda membuktikan saya salah atau memperbaikinya dalam jawaban Anda.
Heriberto Lugo
2
Jika Anda mendeklarasikan variabel sebagai int dan yang lain sebagai Int32, atau bool dan Boolean - klik kanan dan definisi tampilan, Anda akan berakhir dalam definisi yang sama untuk sebuah struct.
Heriberto Lugo
2
@HeribertoLugo benar, baris "Anda harus menghindari menyatakan Jenis Nilai Anda sebagai Bool bukannya bool" keliru. Seperti yang ditunjukkan OP, Anda harus menghindari mendeklarasikan bool Anda (atau Boolean, atau tipe nilai lainnya) sebagai Object. bool / Boolean, int / Int32, hanyalah alias antara C # dan .NET: docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
STW
21

Boxing sebenarnya bukan sesuatu yang Anda gunakan - itu adalah sesuatu yang digunakan runtime sehingga Anda dapat menangani tipe referensi dan nilai dengan cara yang sama bila diperlukan. Misalnya, jika Anda menggunakan ArrayList untuk menyimpan daftar bilangan bulat, bilangan bulat itu kotak agar sesuai dengan slot tipe objek di ArrayList.

Menggunakan koleksi generik sekarang, ini cukup banyak hilang. Jika Anda membuat List<int>, tidak ada tinju yang dilakukan - List<int>dapat menahan bilangan bulat secara langsung.

sinar
sumber
Anda masih membutuhkan tinju untuk hal-hal seperti pemformatan string komposit. Anda mungkin tidak sering melihatnya saat menggunakan obat generik, tetapi pasti masih ada.
Jeremy S
1
benar - itu muncul sepanjang waktu di ADO.NET juga - nilai parameter sql semua 'objek tidak peduli apa tipe data nyata
Ray
11

Boxing dan Unboxing secara khusus digunakan untuk memperlakukan objek tipe nilai sebagai tipe referensi; memindahkan nilai aktual mereka ke tumpukan yang dikelola dan mengakses nilainya dengan referensi.

Tanpa tinju dan unboxing Anda tidak akan pernah bisa melewati tipe-nilai dengan referensi; dan itu berarti Anda tidak bisa meneruskan tipe nilai sebagai instance Object.

STW
sumber
masih jawaban yang bagus setelah hampir 10 tahun pak +1
snr
1
Lulus dengan referensi jenis numerik yang ada dalam bahasa tanpa tinju, dan bahasa lain menerapkan memperlakukan jenis nilai sebagai instance dari objek tanpa tinju dan memindahkan nilai ke tumpukan (misalnya implementasi bahasa dinamis di mana pointer disejajarkan dengan batas 4 byte menggunakan batas bawah empat menggunakan yang lebih rendah empat bit referensi untuk menunjukkan bahwa nilai bilangan bulat atau simbol daripada objek penuh; tipe nilai tersebut tidak dapat diubah dan ukurannya sama dengan pointer).
Pete Kirkham
8

Tempat terakhir saya harus menghapus sesuatu adalah ketika menulis beberapa kode yang mengambil beberapa data dari database (saya tidak menggunakan LINQ ke SQL , hanya ADO.NET tua biasa ):

int myIntValue = (int)reader["MyIntValue"];

Pada dasarnya, jika Anda bekerja dengan API lama sebelum obat generik, Anda akan menemukan tinju. Selain itu, itu tidak umum.

BFree
sumber
4

Diperlukan tinju, ketika kita memiliki fungsi yang membutuhkan objek sebagai parameter, tetapi kita memiliki tipe nilai berbeda yang perlu dilewati, dalam hal itu kita perlu terlebih dahulu mengonversi tipe nilai ke tipe data objek sebelum meneruskannya ke fungsi.

Saya tidak berpikir itu benar, coba ini sebagai gantinya:

class Program
    {
        static void Main(string[] args)
        {
            int x = 4;
            test(x);
        }

        static void test(object o)
        {
            Console.WriteLine(o.ToString());
        }
    }

Itu berjalan dengan baik, saya tidak menggunakan tinju / unboxing. (Kecuali jika kompiler melakukannya di belakang layar?)

Manoj
sumber
Itu karena semuanya mewarisi dari System.Object, dan Anda memberikan metode objek objek dengan informasi tambahan, jadi pada dasarnya Anda memanggil metode pengujian dengan apa yang diharapkan dan apa pun yang mungkin diharapkan karena tidak mengharapkan sesuatu yang khusus. Banyak di .NET dilakukan di belakang layar, dan alasan mengapa itu adalah bahasa yang sangat sederhana untuk digunakan
Mikael Puusaari
1

Di .net, setiap instance Object, atau tipe apa pun yang diturunkan darinya, menyertakan struktur data yang berisi informasi tentang tipenya. Jenis nilai "nyata" dalam .net tidak mengandung informasi semacam itu. Untuk memungkinkan data dalam tipe nilai untuk dimanipulasi oleh rutinitas yang mengharapkan untuk menerima jenis yang berasal dari objek, sistem secara otomatis menentukan untuk setiap jenis nilai tipe kelas yang sesuai dengan anggota dan bidang yang sama. Boxing menciptakan instance baru dari tipe kelas ini, menyalin bidang dari instance tipe nilai. Menghapus kotak menyalin bidang dari turunan tipe kelas ke turunan tipe nilai. Semua tipe kelas yang dibuat dari tipe nilai berasal dari kelas ValueType yang ironisnya dinamai (yang, terlepas dari namanya, sebenarnya adalah tipe referensi).

supercat
sumber
0

Ketika suatu metode hanya menggunakan tipe referensi sebagai parameter (katakanlah metode generik dibatasi menjadi kelas melalui newkendala), Anda tidak akan bisa meneruskan tipe referensi ke sana dan harus mengotakkannya.

Ini juga berlaku untuk semua metode yang digunakan object parameter - ini harus menjadi tipe referensi.

Oded
sumber
0

Secara umum, Anda biasanya ingin menghindari bertinju tipe nilai Anda.

Namun, ada kejadian langka di mana ini berguna. Jika Anda perlu menargetkan kerangka kerja 1.1, misalnya, Anda tidak akan memiliki akses ke koleksi umum. Setiap penggunaan koleksi di. NET 1.1 akan memerlukan memperlakukan jenis nilai Anda sebagai System.Object, yang menyebabkan tinju / unboxing.

Masih ada kasus untuk ini berguna di .NET 2.0+. Setiap kali Anda ingin memanfaatkan fakta bahwa semua tipe, termasuk tipe nilai, dapat diperlakukan sebagai objek secara langsung, Anda mungkin perlu menggunakan tinju / unboxing. Kadang-kadang ini berguna, karena memungkinkan Anda untuk menyimpan tipe apa pun dalam koleksi (dengan menggunakan objek alih-alih T dalam koleksi generik), tetapi secara umum, lebih baik untuk menghindari ini, karena Anda kehilangan keamanan jenis. Satu-satunya kasus di mana tinju sering terjadi, adalah ketika Anda menggunakan Refleksi - banyak panggilan dalam refleksi akan membutuhkan tinju / unboxing ketika bekerja dengan tipe nilai, karena tipe tersebut tidak diketahui sebelumnya.

Hunain
sumber
0

Tinju adalah konversi nilai ke tipe referensi dengan data di beberapa offset dalam objek di heap.

Adapun apa yang sebenarnya dilakukan tinju. Berikut ini beberapa contohnya

Mono 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;

Membuka kotak di Mono adalah proses casting pointer pada offset 2 gpointer pada objek (mis. 16 byte). A gpointeradalah a void*. Ini masuk akal ketika melihat definisiMonoObject seperti itu jelas hanya header untuk data.

C ++

Untuk mengotakkan nilai dalam C ++ Anda bisa melakukan sesuatu seperti:

#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;
}
Lewis Kelsey
sumber