Belajar Kelas dan Struktur di Swift Praktik Langsung Sampai Kamu Paham

Belajar Kelas dan Struktur di Swift Praktik Langsung Sampai Kamu Paham
Photo by Hal Gatewood/Unsplash

Dunia pengembangan aplikasi itu ibarat membangun sebuah kota. Kalau kamu lagi belajar Swift buat bikin aplikasi di iPhone atau iPad, berarti kamu lagi belajar jadi arsitek dan insinyur di kota digital itu. Nah, di kota digital ini, ada dua material bangunan paling dasar yang wajib kamu kuasai: Kelas (Classes) dan Struktur (Structs). Dua-duanya sama-sama bisa jadi "cetak biru" buat objek atau data di aplikasi kamu, tapi cara kerjanya beda banget dan kalau salah pilih bisa bikin pusing tujuh keliling.

Makanya, di artikel ini kita bakal bongkar tuntas kelas dan struktur di Swift, mulai dari teori sampai praktik langsung, biar kamu benar-benar paham kapan harus pakai yang mana. Santai aja, kita bakal ngobrolin ini dengan bahasa yang gampang dicerna, kayak lagi nongkrong bareng temen. Siap? Yuk, kita mulai!

Pendahuluan: Kenapa Kelas dan Struktur Itu Penting Banget?

Coba bayangin kamu lagi bikin aplikasi daftar belanjaan. Kamu pasti butuh cara buat merepresentasikan satu item belanjaan, kan? Misalnya, ada nama barang, harga, dan jumlah. Nah, kelas atau struktur inilah yang bakal jadi "wadah" buat menyimpan informasi itu. Mereka adalah fondasi utama buat ngorganisir kode kamu biar lebih rapi, mudah dibaca, dan gampang dikelola. Tanpa mereka, kode kamu bakal jadi spaghetti yang susah diurai.

Singkatnya, baik kelas maupun struktur sama-sama bisa:

  • Mendefinisikan properti (alias data yang disimpan)
  • Mendefinisikan metode (alias fungsi yang bisa dilakukan oleh objek)
  • Mendefinisikan subscript (cara akses nilai pakai kurung siku, kayak array atau dictionary)
  • Mendefinisikan initializer (cara bikin objek baru)
  • Bisa diperluas (extension)
  • Bisa pakai protokol (protocol conformance)

Terus, kalau sama-sama bisa ngelakuin banyak hal itu, apa bedanya? Nah, ini dia intinya! Perbedaan paling fundamental antara kelas dan struktur itu ada di cara mereka di-handle memori: Kelas itu reference type (tipe referensi), sedangkan Struktur itu value type (tipe nilai). Kedengarannya ribet? Tenang, nanti kita bedah satu per satu sampai kamu ngerti.

1. Menggali Lebih Dalam: Kelas (Classes) – Si Tipe Referensi

Bayangkan kelas itu seperti denah rumah yang cuma ada satu copy-nya di kantor arsitek. Kalau kamu bikin rumah dari denah itu, kamu dapat "rumah fisik" yang terhubung langsung ke denah asli. Kalau ada perubahan di rumah fisik itu, arsitek tahu karena dia punya referensi ke rumah kamu.

Dalam Swift, kelas itu adalah blueprint untuk objek yang berjenis tipe referensi. Artinya, kalau kamu bikin sebuah instance (atau objek) dari sebuah kelas, kamu tidak punya salinan data yang terpisah. Sebaliknya, kamu punya "referensi" atau "pointer" yang menunjuk ke lokasi data objek itu di memori komputer (lebih tepatnya, di bagian memori yang disebut "heap").

Karakteristik Utama Kelas:

  • Reference Type: Ini dia yang paling penting! Ketika kamu menyalin sebuah instance kelas ke variabel lain, kamu tidak menyalin data objeknya, melainkan hanya menyalin referensi ke objek yang sama di memori. Jadi, kalau kamu mengubah data lewat salah satu variabel, perubahan itu akan terlihat juga di variabel lain yang menunjuk ke objek yang sama.
  • Pewarisan (Inheritance): Kelas bisa mewarisi karakteristik dari kelas lain (kelas induk/superclass). Ini memungkinkan kamu membuat hierarki kelas dan menggunakan ulang kode. Misalnya, kelas Mobil bisa mewarisi dari kelas Kendaraan.
  • Deinitializer: Kelas punya deinit yang dipanggil ketika sebuah instance kelas akan dihapus dari memori. Ini berguna untuk membersihkan sumber daya yang mungkin dipegang oleh objek tersebut (meskipun Swift dengan Automatic Reference Counting (ARC) seringkali mengurus ini secara otomatis).
  • Identitas Referensi: Kamu bisa mengecek apakah dua instance kelas mengacu pada objek yang sama di memori menggunakan operator identitas === (sama dengan secara referensi) dan !== (tidak sama dengan secara referensi).

Contoh Kelas Sederhana:

swift
class Manusia {
    var nama: String
    var umur: Int
    var tinggiBadan: Double // dalam cm// Initializer: Cara bikin objek Manusia
    init(nama: String, umur: Int, tinggiBadan: Double) {
        self.nama = nama
        self.umur = umur
        self.tinggiBadan = tinggiBadan
    }// Metode: Fungsi yang bisa dilakukan objek Manusia
    func sapa() {
        print("Halo, nama saya \(nama), umur saya \(umur) tahun.")
    }func tambahUmur() {
        self.umur += 1
        print("\(nama) sekarang berumur \(umur) tahun.")
    }// Deinitializer: Dipanggil saat objek dihapus dari memori
    deinit {
        print("\(nama) sudah tidak ada lagi di memori.")
    }
}// Membuat instance dari kelas Manusia
var budi = Manusia(nama: "Budi", umur: 25, tinggiBadan: 170.5)
budi.sapa() // Output: Halo, nama saya Budi, umur saya 25 tahun.
budi.tambahUmur() // Output: Budi sekarang berumur 26 tahun.// Contoh efek Reference Type
print("--- Efek Reference Type ---")
var temanBudi = budi // temanBudi sekarang menunjuk ke objek yang sama dengan budiprint("Umur Budi (awal): \(budi.umur)") // Output: Umur Budi (awal): 26
print("Umur Teman Budi (awal): \(temanBudi.umur)") // Output: Umur Teman Budi (awal): 26temanBudi.umur = 30 // Mengubah umur lewat temanBudi
print("Umur Budi (setelah diubah via temanBudi): \(budi.umur)") // Output: Umur Budi (setelah diubah via temanBudi): 30
print("Umur Teman Budi (setelah diubah via temanBudi): \(temanBudi.umur)") // Output: Umur Teman Budi (setelah diubah via temanBudi): 30// Mengecek identitas referensi
if budi === temanBudi {
    print("Budi dan TemanBudi adalah objek yang sama di memori.")
} else {
    print("Budi dan TemanBudi adalah objek yang berbeda.")
}
// Output: Budi dan TemanBudi adalah objek yang sama di memori.

Dari contoh di atas, kamu bisa lihat betapa pentingnya konsep reference type. Kalau kamu tidak paham ini, bisa-bisa data kamu berubah tanpa diduga karena ada variabel lain yang ikut menunjuk ke objek yang sama.

2. Menggali Lebih Dalam: Struktur (Structs) – Si Tipe Nilai

Nah, kalau struktur ini beda lagi. Bayangkan struktur itu seperti lembar resep kue. Kamu bisa menyalin resep itu berkali-kali ke teman-temanmu. Setiap teman akan punya salinan resepnya sendiri. Kalau salah satu teman mengubah resepnya (misalnya, nambahin cokelat chip), resep aslimu atau resep teman yang lain tidak akan ikut berubah.

Dalam Swift, struktur adalah blueprint untuk objek yang berjenis tipe nilai. Ini berarti ketika kamu membuat sebuah instance dari struktur, dan kemudian menyalin instance itu ke variabel lain, Swift akan membuat salinan data yang benar-benar terpisah dari instance aslinya. Setiap variabel punya copy datanya sendiri di memori (biasanya di "stack" untuk struct-struct kecil, tapi bisa juga di "heap" untuk struct yang besar atau yang berisi reference type lain).

Karakteristik Utama Struktur:

  • Value Type: Ini dia perbedaannya! Ketika kamu menyalin sebuah instance struktur ke variabel lain, seluruh data dari instance itu disalin. Jadi, kedua variabel akan memiliki salinan data yang independen. Perubahan pada satu salinan tidak akan memengaruhi salinan lainnya.
  • Tanpa Pewarisan: Struktur tidak bisa mewarisi dari struktur atau kelas lain. Mereka adalah tipe data mandiri.
  • Initializer Anggota Otomatis (Automatic Memberwise Initializer): Swift secara otomatis menyediakan initializer yang bisa kamu gunakan untuk menginisialisasi semua properti yang disimpan di struktur kamu. Ini praktis banget!
  • Immutability Default: Jika kamu mendeklarasikan sebuah instance struktur sebagai konstanta (let), maka kamu tidak bisa mengubah properti apa pun dari instance tersebut. Kalau kamu ingin mengubah properti, instance harus dideklarasikan sebagai variabel (var) dan metodenya harus ditandai dengan keyword mutating.

Contoh Struktur Sederhana:

swift
struct Titik {
    var x: Double
    var y: Double// Swift secara otomatis menyediakan:
    // init(x: Double, y: Double) { self.x = x; self.y = y }// Metode yang mengubah properti harus ditandai 'mutating'
    mutating func pindah(dx: Double, dy: Double) {
        self.x += dx
        self.y += dy
        print("Titik pindah ke (\(x), \(y))")
    }func deskripsi() {
        print("Titik ini ada di (\(x), \(y))")
    }
}// Membuat instance dari struktur Titik
var titikA = Titik(x: 10.0, y: 20.0)
titikA.deskripsi() // Output: Titik ini ada di (10.0, 20.0)// Contoh efek Value Type
print("--- Efek Value Type ---")
var titikB = titikA // titikB sekarang punya salinan data dari titikAprint("Titik A (awal): (\(titikA.x), \(titikA.y))") // Output: Titik A (awal): (10.0, 20.0)
print("Titik B (awal): (\(titikB.x), \(titikB.y))") // Output: Titik B (awal): (10.0, 20.0)titikA.x = 15.0 // Mengubah x lewat titikA
titikA.pindah(dx: 2.0, dy: 3.0) // Mengubah titikA via mutating method
// Output: Titik pindah ke (17.0, 23.0)print("Titik A (setelah diubah): (\(titikA.x), \(titikA.y))") // Output: Titik A (setelah diubah): (17.0, 23.0)
print("Titik B (setelah diubah titikA): (\(titikB.x), \(titikB.y))") // Output: Titik B (setelah diubah titikA): (10.0, 20.0)
// Titik B tetap sama! Karena dia punya salinan datanya sendiri.

Dari contoh di atas, perbedaan antara value type dan reference type jadi sangat jelas, kan? Saat titikA diubah, titikB sama sekali tidak terpengaruh karena titikB adalah salinan yang independen.

3. Pertarungan Sengit: Kelas vs. Struktur – Kapan Pakai yang Mana?

Ini dia pertanyaan jutaan dolar di dunia Swift! Memilih antara kelas dan struktur itu bukan cuma masalah selera, tapi ada pertimbangan teknis yang lumayan krusial.

| Fitur / Karakteristik | Kelas (Classes) | Struktur (Structs) | | :------------------------------ | :------------------------------------------------------ | :----------------------------------------------------- | | Jenis Tipe | Reference Type | Value Type | | Penyalinan | Menyalin referensi (alamat memori) | Menyalin data keseluruhan | | Pewarisan | Bisa (bisa punya subclass) | Tidak bisa | | Deinitializer | Punya (deinit) | Tidak punya (tidak perlu, ARC urus otomatis) | | Identitas Referensi | Ada (===, !==) | Tidak ada (cukup cek kesamaan nilai ==) | | Memberwise Initializer | Tidak otomatis (harus bikin manual atau inisialisasi semua properti) | Otomatis disediakan oleh Swift | | Perubahan Properti | Properti bisa diubah jika instance var atau let | Properti bisa diubah hanya jika instance var dan metodenya mutating | | Interoperabilitas Obj-C | Lebih baik untuk berinteraksi dengan Objective-C | Kurang cocok untuk Objective-C | | Lokasi Memori Umumnya | Heap | Stack (untuk data kecil), Heap (untuk data besar/nested reference types) |

Kapan Kamu Sebaiknya Menggunakan Struktur (Structs)?

Secara umum, Swift merekomendasikan untuk lebih sering menggunakan struktur sampai kamu benar-benar membutuhkan fitur yang hanya ada di kelas. Ini dikenal sebagai "prefer structs over classes" atau "value semantics by default".

Gunakan struktur ketika:

  • Merepresentasikan data sederhana: Misalnya, koordinat 2D (seperti Titik kita tadi), ukuran (Size), warna (Color), tanggal (Date), atau item dalam daftar (TodoItem).
  • Data itu harus disalin saat ditugaskan atau diteruskan: Kamu ingin setiap variabel memiliki salinan data yang independen, sehingga perubahan pada satu variabel tidak memengaruhi yang lain.
  • Tidak perlu pewarisan: Tipe data tersebut tidak perlu memiliki subclass atau diwarisi dari tipe lain.
  • Ukuran objek kecil: Untuk objek-objek kecil, struktur seringkali lebih efisien dalam hal kinerja karena penyalinan data yang cepat dan alokasi memori yang lebih sederhana (sering di stack).
  • Data bersifat Immutable (tidak bisa diubah): Kalau kamu membuat instance struktur pakai let, maka semua propertinya tidak bisa diubah. Ini bagus buat keamanan data.

Kamu ingin thread-safety yang lebih mudah: Karena struktur disalin, kamu tidak perlu khawatir tentang beberapa thread yang mencoba memodifikasi objek yang sama secara bersamaan, mengurangi potensi race condition*.

Contoh Struktur Populer di Swift Standard Library:

  • String
  • Array
  • Dictionary
  • Int, Double, Bool
  • Optional

Ya, betul! Tipe-tipe data yang sering kamu pakai ini adalah struktur! Itu kenapa kalau kamu copy sebuah array, kamu dapat salinan yang terpisah.

Kapan Kamu Sebaiknya Menggunakan Kelas (Classes)?

Gunakan kelas ketika:

  • Membutuhkan pewarisan (inheritance): Jika kamu perlu membuat hierarki tipe data, di mana satu tipe bisa mewarisi karakteristik dari tipe lain (misalnya, Mobil mewarisi dari Kendaraan).
  • Membutuhkan interoperabilitas dengan Objective-C: Jika kamu berinteraksi dengan framework Cocoa atau Cocoa Touch yang ditulis dalam Objective-C (misalnya UIViewController, UIView), mereka seringkali mengharapkan kelas.
  • Membutuhkan identitas referensi: Jika penting bagi kamu untuk mengetahui apakah dua variabel menunjuk ke instance objek yang sama persis di memori, bukan hanya data yang sama. Contohnya Manager atau Service yang hanya boleh ada satu instance.

Mengelola sumber daya yang dibagikan: Misalnya, sebuah objek yang mengelola koneksi database atau file handler* yang harus diakses dan dimodifikasi oleh beberapa bagian dari aplikasi secara bersamaan.

  • Perilaku mutasi (perubahan) yang diharapkan: Ketika perubahan pada satu objek harus terlihat di semua tempat yang memiliki referensi ke objek tersebut. Contohnya, model data yang diakses dari berbagai View Controller.

Contoh Kelas Populer di Framework Apple:

  • UIViewController
  • UIView
  • UILabel, UIButton, dll.
  • NSPerson (dari Foundation framework)

4. Praktik Langsung Sampai Kamu Paham!

Oke, sekarang kita akan coba bikin contoh yang lebih kompleks, biar kamu bisa ngerasain sendiri bedanya.

Skenario 1: Struktur untuk Data Barang Belanjaan

Kita pakai struktur untuk merepresentasikan BarangBelanjaan karena ini adalah tipe data sederhana yang kita mau pastikan kalau disalin, salinannya independen.

swift
struct BarangBelanjaan {
    let nama: String // tidak bisa diubah setelah dibuat
    var hargaSatuan: Double
    var jumlah: Intvar totalHarga: Double { // Computed Property
        return hargaSatuan * Double(jumlah)
    }// Metode untuk mengubah jumlah (karena jumlah bisa berubah, perlu 'mutating')
    mutating func tambahJumlah(berapa: Int) {
        self.jumlah += berapa
        print("\(nama) sekarang berjumlah \(self.jumlah)")
    }mutating func ubahHarga(hargaBaru: Double) {
        self.hargaSatuan = hargaBaru
        print("Harga satuan \(nama) sekarang \(self.hargaSatuan)")
    }
}print("\n=== Praktik Struct: BarangBelanjaan ===")
var apel = BarangBelanjaan(nama: "Apel Fuji", hargaSatuan: 20000.0, jumlah: 3)
apel.deskripsi() // Metode 'deskripsi' harus kita buat sendiri kalau mau, atau pakai print biasa
print("Total harga apel: Rp \(apel.totalHarga)")var apelDiskon = apel // Menyalin apel ke apelDiskon
print("Apel (awal): \(apel.jumlah) buah, total Rp \(apel.totalHarga)")
print("Apel Diskon (awal): \(apelDiskon.jumlah) buah, total Rp \(apelDiskon.totalHarga)")// Kita ubah apelDiskon, apel asli tidak akan terpengaruh
apelDiskon.hargaSatuan = 15000.0
apelDiskon.tambahJumlah(berapa: 2) // Output: Apel Fuji sekarang berjumlah 5print("Apel (setelah ubah apelDiskon): \(apel.jumlah) buah, total Rp \(apel.totalHarga)")
print("Apel Diskon (setelah ubah): \(apelDiskon.jumlah) buah, total Rp \(apelDiskon.totalHarga)")

Skenario 2: Kelas untuk Sistem Inventori Toko

Kita pakai kelas untuk ProdukToko dan SistemInventori karena ini lebih kompleks, mungkin butuh pewarisan (misal ProdukElektronik dari ProdukToko), dan kita ingin SistemInventori ini menjadi objek tunggal yang diakses dan dimodifikasi oleh berbagai bagian aplikasi, jadi perubahan di satu tempat akan terlihat di semua tempat.

swift
class ProdukToko {
    var id: String
    var nama: String
    var harga: Double
    var stok: Intinit(id: String, nama: String, harga: Double, stok: Int) {
        self.id = id
        self.nama = nama
        self.harga = harga
        self.stok = stok
    }func infoProduk() {
        print("Produk ID: \(id), Nama: \(nama), Harga: Rp \(harga), Stok: \(stok)")
    }func jualProduk(jumlahTerjual: Int) {
        if stok >= jumlahTerjual {
            stok -= jumlahTerjual
            print("\(jumlahTerjual) \(nama) berhasil terjual. Sisa stok: \(stok)")
        } else {
            print("Stok \(nama) tidak cukup. Tersedia: \(stok)")
        }
    }
}// Contoh pewarisan kelas
class ProdukElektronik: ProdukToko {
    var garansiBulan: Intinit(id: String, nama: String, harga: Double, stok: Int, garansiBulan: Int) {
        self.garansiBulan = garansiBulan
        super.init(id: id, nama: nama, harga: harga, stok: stok)
    }override func infoProduk() { // Override metode dari parent class
        super.infoProduk()
        print("Garansi: \(garansiBulan) bulan.")
    }
}class SistemInventori {
    var daftarProduk: [ProdukToko] // Array berisi objek kelas ProdukTokoinit() {
        self.daftarProduk = []
    }func tambahProduk(produk: ProdukToko) {
        daftarProduk.append(produk)
        print("\(produk.nama) berhasil ditambahkan ke inventori.")
    }func cariProduk(id: String) -> ProdukToko? {
        return daftarProduk.first { $0.id == id }
    }func tampilkanSemuaProduk() {
        print("\n--- Daftar Produk di Inventori ---")
        if daftarProduk.isEmpty {
            print("Inventori kosong.")
            return
        }
        for produk in daftarProduk {
            produk.infoProduk()
        }
        print("-------------------------------")
    }
}print("\n=== Praktik Class: SistemInventori ===")
let sistemToko = SistemInventori() // Instance SistemInventori
let laptop = ProdukToko(id: "LPT001", nama: "Laptop XYZ", harga: 12500000.0, stok: 10)
let keyboard = ProdukElektronik(id: "KBD005", nama: "Keyboard Gaming", harga: 800000.0, stok: 25, garansiBulan: 12)sistemToko.tambahProduk(produk: laptop)
sistemToko.tambahProduk(produk: keyboard)
sistemToko.tampilkanSemuaProduk()// Mendapatkan referensi ke laptop dari sistemToko
if let laptopDariSistem = sistemToko.cariProduk(id: "LPT001") {
    print("\n--- Operasi di Laptop dari Sistem ---")
    laptopDariSistem.jualProduk(jumlahTerjual: 2) // Mengubah stok laptop yang ada di sistem
}sistemToko.tampilkanSemuaProduk() // Cek, apakah stok laptop berubah? Ya, pasti berubah!// Coba buat referensi lain ke laptop
let laptopLain = laptop
print("\n--- Operasi di Referensi Lain ---")
laptopLain.jualProduk(jumlahTerjual: 3) // Mengubah stok laptop via referensi lain

Dari contoh ini, kamu bisa lihat:

  • ProdukElektronik bisa mewarisi dari ProdukToko (fitur kelas).

Ketika laptopDariSistem dan laptopLain memodifikasi objek laptop, perubahan itu langsung tercermin di mana pun referensi ke objek laptop itu ada, termasuk di dalam sistemToko.daftarProduk. Ini kekuatan (dan bahaya) dari reference type*.

Tips Tambahan biar Makin Jago:

  1. Prioritaskan Struktur (Value Types): Mulai dengan struktur kecuali ada alasan kuat untuk menggunakan kelas. Ini akan membuat kode kamu lebih aman dari efek samping yang tidak terduga karena perubahan data.
  2. Pahami Mutability (Kemampuan Berubah): Dengan struktur, properti hanya bisa diubah jika instance-nya var dan metodenya mutating. Dengan kelas, properti bisa diubah bahkan jika instance-nya let (selama propertinya var), tapi referensi objeknya tidak bisa diubah.
  3. Hati-hati dengan Shared State di Kelas: Karena kelas itu reference type, beberapa bagian kode bisa menunjuk ke objek yang sama. Kalau satu bagian mengubah objek itu, bagian lain akan melihat perubahannya. Ini bisa jadi sumber bug yang sulit ditemukan kalau tidak hati-hati.
  4. Pelajari ARC (Automatic Reference Counting): Untuk kelas, Swift menggunakan ARC untuk melacak berapa banyak referensi yang menunjuk ke sebuah objek. Ketika tidak ada lagi referensi, objek akan dihapus dari memori. Pemahaman dasar tentang ini penting untuk menghindari memory leak atau retain cycle (meskipun ini topik yang lebih lanjut).

Penutup: Kunci Menguasai Swift

Memahami perbedaan mendasar antara kelas dan struktur adalah salah satu langkah paling krusial dalam perjalanan kamu menguasai Swift. Ini bukan cuma soal sintaks, tapi soal bagaimana data kamu dikelola dalam memori, bagaimana performa aplikasi kamu, dan seberapa mudah kode kamu di-debug.

Dengan praktik langsung dan pemahaman yang kuat tentang value type vs reference type, kamu bakal lebih percaya diri dalam mendesain arsitektur aplikasi Swift yang rapi, efisien, dan mudah dipelihara. Jadi, teruslah bereksperimen, tulis banyak kode, dan jangan takut salah. Setiap kesalahan adalah pelajaran berharga! Selamat ngoding!