İçeriğe geç
academia.sh

Ders 14 / 18

Dizgiler ve Rune'lar

Bir Go dizgisi bir bayt dizisidir: uzunluk bayt sayar, indeksleme bir bayt döndürür, yalnızca range bir rune verir. Aynı metnin bayt sayısı, rune sayısı ve ilk baytı birbirinden ayrı ölçülüyor ve dizginin değiştirilemez olduğu gösteriliyor. Sınırlayıcı ölçüm rune sayısının da karakter sayısı olmadığını gösteriyor: birleşik işaretler görünüşte aynı metni farklı rune sayılarıyla üretebiliyor.

İçindekiler

Bu konu boyunca ölçülen tipler — dizi, dilim, sözlük, yapı, işaretçi — hep sayı, adres ya da adlandırılmış alan taşıdı. Dizgi bunlardan ayrı bir soru soruyor: dışarıdan bakıldığında tek parça görünen bir metin, içeride ne taşıyor? Yanıt, bu konunun kapanış sorusu: dizginin uzunluğu neyi sayıyor, bir dizgiyi indekslemek neyi döndürüyor?

Go’nun dizgi tipi, dilin standart kitaplığındaki metin işleme araçlarının hepsinin altında duruyor: bir dosya yolu, bir ağ isteğinin gövdesi, bir kullanıcı girdisi — hepsi dizgi olarak taşınıyor. Bu tipin ne taşıdığını bilmemek, bu araçların hangi birimde çalıştığını (bayt mı, kod noktası mı) bilmemek anlamına geliyor.

Dizgi de bir bileşik tiptir, ama bu konudaki ötekilerden iki yönden ayrılıyor. Birincisi, şimdiye kadar ölçülen her tip (dizi hariç) bir şekilde değiştirilebilirdi — dilime yazılabiliyordu, sözlüğe eklenebiliyordu, yapının alanı güncellenebiliyordu; dizgi hiçbir biçimde yerinde değiştirilemiyor. İkincisi, dizginin taşıdığı veri kendi başına anlamsız baytlar değil, bir kodlama kuralına (UTF-8) göre yorumlanan bir bayt dizisi; bu kural, uzunluğun ve indekslemenin ne anlama geldiğini doğrudan belirliyor.

Dizgi Bir Bayt Dizisidir

Go’da bir dizgi, UTF-8 ile kodlanmış salt okunur bir bayt dizisidir. len bir dizgiye uygulandığında bayt sayısını verir, indeksleme (metin[i]) tek bir baytı döndürür — bir karakteri değil. Bu, Sıfır Değerler dersindeki bağlayıcı dağılımın açıkta kalan üç durumundan biriydi: dizginin uzunluğu tanımlı ama indekslendiğinde gelen şey karakter değil bayttır.

BT16."çay" özellikle Türkçe bir sözcük olarak seçildi, çünkü ç gibi tek baytla kodlanamayan bir harf içeriyor; yalnızca ASCII harflerinden oluşan bir metin bu ölçümdeki bayt/rune farkını hiç göstermezdi.

// bayt.go — dizginin uzunlugu ve indekslemesi bayt uzerinden calisir
package main

import "fmt"

func main() {
	metin := "çay"
	fmt.Println("uzunluk (bayt):", len(metin))
	fmt.Println("ilk bayt      :", metin[0])
	fmt.Println("rune sayisi   :", len([]rune(metin)))
}
uzunluk (bayt): 4
ilk bayt      : 195
rune sayisi   : 3

"çay" üç harf gibi görünüyor ama len 4 veriyor, çünkü ç harfi UTF-8’de iki bayta kodlanıyor (195 ve bir sonraki bayt), a ile y birer bayta. metin[0] bir karakter değil, çnin ilk baytının sayısal değerini (195) veriyor — fmt.Println bunu bir tam sayı olarak basıyor, dizgi parçası olarak değil. []rune(metin) dönüşümü ise dizgiyi kod noktalarına ayırıyor ve üç rune buluyor — görünen harf sayısıyla örtüşen sayı bu, bayt sayısı değil.

Bu üç sayının (4, 195, 3) birbirinden bağımsız okunması gerekiyor. len(metin) her zaman baytı sayar, kaç harf görünürse görünsün; salt ASCII bir dizgide bayt sayısı ile rune sayısı çakışır, ama bu bir rastlantıdır — ASCII’nin her karakteri zaten tek bayta kodlandığı için. Türkçe, Yunanca, Kiril ya da herhangi bir çok baytlı alfabe içeren bir dizgide bu çakışma bozuluyor; "çay" üç harfken dört bayt olması tam olarak bu bozulmanın kendisi.

Dizgi Değiştirilemez

İşaretçiler dersinin sonunda bırakılan soru burada yanıtını buluyor: dizgi bu konudaki kopya/paylaşım ayrımının dışında kalıyor, çünkü dizgi değiştirilemez. Bir dizginin tek bir baytını bile yerinde değiştirmek mümkün değil. Bu, dilim ile dizginin arasındaki en görünür farktır: ikisi de bir bayt (ya da öğe) dizisine giden bir başlık taşır, ama dilimin başlığının gösterdiği alt diziye yazmak serbestken, dizginin başlığının gösterdiği bayt dizisine yazmak dilin kendisi tarafından engellenir.

// degistirilemez.go — dizginin bir baytina yazmak reddedilir
package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
)

func gecerMi(kaynak string) bool {
	dizin, err := os.MkdirTemp("", "sinama")
	if err != nil {
		return false
	}
	defer os.RemoveAll(dizin)
	os.WriteFile(filepath.Join(dizin, "go.mod"), []byte("module sinama\n\ngo 1.24\n"), 0o644)
	os.WriteFile(filepath.Join(dizin, "kod.go"), []byte(kaynak), 0o644)
	cmd := exec.Command("go", "build", "./...")
	cmd.Dir = dizin
	cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
	return cmd.Run() == nil
}

func main() {
	yazma := `package sinama
	func F() {
		s := "abc"
		s[0] = 'x'
		_ = s
	}`
	fmt.Println("dizgi indeksine yazma:", gecerMi(yazma))
}
dizgi indeksine yazma: false

s[0] bir bayt okumak için geçerli bir yazım, ama s[0] = 'x' reddediliyor — derleyici dizginin öğelerine yazmaya izin vermiyor. Bu, Diziler ve Dilimler dersindeki dizi indeksine yazmaktan temelde farklı: dizi öğesine yazmak geçerlidir, dizgi baytına yazmak hiçbir zaman geçerli değildir. Bir dizgiyi “değiştirmek” isteyen kod, aslında yeni bir dizgi üretiyor — orijinali olduğu gibi bırakıp, değiştirilmiş bir kopyayı bir değişkene yeniden atıyor. Bu yüzden dizgi, bu konudaki hiçbir derste ölçülen “türetilen bir parçaya yazınca kaynak da değişiyor mu” sorusuna hiç muhatap olmuyor — yazmanın kendisi mümkün değil.

BT17. — Bu ölçüm de bir derleyici çağrısıyla koşturuluyor, çünkü ret derleme zamanında geliyor; çalışma zamanına bırakılan bir panik değil.

Bir dizgiyi baytlar üzerinde değiştirmek isteyen kod, []byte dönüşümüyle ayrı bir kopya çıkarıyor; o kopya değiştirilebilir ama orijinal dizgi bundan etkilenmiyor.

// donusum.go — []byte donusumu ayri bir kopya cikarir
package main

import "fmt"

func main() {
	metin := "çay"
	baytlar := []byte(metin)
	baytlar[2] = 'X' // ucuncu bayt 'a' harfidir, tek bayt kaplar
	fmt.Println("degistirilen kopya:", string(baytlar))
	fmt.Println("orijinal dizgi    :", metin)

	parca1 := "çay"
	birlesik := parca1 + " bahcesi"
	fmt.Println("birlesik dizgi    :", birlesik)
	fmt.Println("parca1 degismedi mi:", parca1 == "çay")
}
degistirilen kopya: çXy
orijinal dizgi    : çay
birlesik dizgi    : çay bahcesi
parca1 degismedi mi: true

[]byte(metin) yazımı metinin baytlarını yeni bir dilime kopyalıyor; bu dilim sıradan bir dilim gibi değiştirilebilir, üçüncü baytını 'X' yapmak dilimin kendi verisini değiştiriyor. Ama metin bundan hiç etkilenmiyor, çünkü dönüşüm bir paylaşım değil bir kopyalama. + ile birleştirme de benzer: parca1 + " bahcesi" yeni bir dizgi üretiyor, parca1in kendisi hiç değişmiyor. Diziler ve Dilimler dersindeki dizi gibi, dizgi de “türetilmiş” hiçbir yolla kaynağa geri yazılamıyor — dilim ve sözlükten ayrıldığı yer tam olarak burası.

range: Bayt İndeksi, Rune Değeri

len ve indeksleme bayt üzerinden çalışıyordu; bir dizgi üzerinde range ile gezinmek ise farklı davranıyor. range, dizgiyi UTF-8 kurallarına göre çözüyor ve her adımda bir rune veriyor, tek bir bayt değil. len bayt sayıyor, metin[i] bir bayt veriyor, range bir rune veriyor — üçü de aynı veriden, üç ayrı birimde okuyor.

// gezinme.go — range dizgide bayt degil rune verir
package main

import "fmt"

func main() {
	metin := "çay"
	for i, r := range metin {
		fmt.Printf("  bayt-indeks=%d rune=%q kod-noktasi=U+%04X\n", i, r, r)
	}
}
  bayt-indeks=0 rune='ç' kod-noktasi=U+00E7
  bayt-indeks=2 rune='a' kod-noktasi=U+0061
  bayt-indeks=3 rune='y' kod-noktasi=U+0079

Üç adım, üç rune veriyor — metin[0]in verdiği tek bayt (195) değil, tam bir kod noktası (ç, U+00E7). Basılan indeksler de ardışık değil: 0, 2, 3 — ikinci adım 1i atlıyor, çünkü ç iki bayt kaplıyor ve range bir sonraki rune’un bayt konumuna atlıyor. rune, dilin kendi tip adıdır ve int32nin bir takma adıdır; bir Unicode kod noktasını taşır. Kod noktasının kendisi Bilgisayarlar Nasıl Çalışır dersinde kapatılmış bir kavramdır; burada ölçülen, Go’nun bu kavramı rune adıyla ve rangein otomatik çözümüyle nasıl gerçekleştirdiğidir.

Bu otomatik çözüm yalnızca rangee özgü; dizgi başka bir bileşik tipe dönüştürülerek gezinilirse aynı çözüm uygulanmıyor. []byte(metin) üzerinde range yapmak, metinin kendisi üzerinde range yapmaktan farklı sonuç veriyor.

// bayt-gezinme.go — []byte uzerinde range bayt bayt gezinir
package main

import "fmt"

func main() {
	metin := "çay"
	for i, b := range []byte(metin) {
		fmt.Printf("  indeks=%d bayt=%d\n", i, b)
	}
}
  indeks=0 bayt=195
  indeks=1 bayt=167
  indeks=2 bayt=97
  indeks=3 bayt=121

Burada dört adım var, önceki bölümdeki üç adım değil; indeksler 0, 1, 2, 3 — ardışık, atlamasız. rangein rune’lara çözme davranışı yalnızca rangee verilen değer bir dizgi olduğunda geçerli; aynı veriyi []byteye çevirip verdiğinde range sıradan bir dilim üzerinde gezindiğini varsayıyor ve tek tek baytları veriyor. Fark kaynaktaki verinin kendisinde değil, rangee verilen tipte.

Sınırlayıcı Ölçüm: Rune Sayısı da Karakter Sayısı Değildir

len([]rune(metin)) görünen harf sayısını verdi ve bu, bayt sayısından daha güvenilir görünüyor. Ama rune sayısı da “karakter sayısı” ile her zaman örtüşmüyor, çünkü Unicode’da görünüşte tek bir işaret, birden çok kod noktasının birleşimiyle de yazılabiliyor.

BT18. — İki dizgi de kaynakta birebir aynı görünen dört harfi (c, a, f, e-türevi bir işaret) taşıyor; ölçüm ikisini yalnızca kod noktası düzeyinde ayırıyor, ekrandaki görüntüyü değil.

// birlesim.go — gorunuste ayni metin, iki ayri rune sayisi
package main

import "fmt"

func main() {
	onceden := "café"    // e ve aksan tek kod noktasinda birlesik: U+00E9
	sonradan := "cafe" + "́" // e ayri, aksan ayri kod noktasi: U+0065 U+0301
	fmt.Println("onceden bilesimli:", onceden, " rune sayisi=", len([]rune(onceden)), " bayt=", len(onceden))
	fmt.Println("sonradan bilesen :", sonradan, " rune sayisi=", len([]rune(sonradan)), " bayt=", len(sonradan))
	fmt.Println("bayt bayt esit mi:", onceden == sonradan)
}
onceden bilesimli: café  rune sayisi= 4  bayt= 5
sonradan bilesen : café  rune sayisi= 5  bayt= 6
bayt bayt esit mi: false

İki dizgi de ekranda aynı görünüyor — ikisi de “café” yazıyor. Ama onceden tek bir önceden birleşik kod noktası (é, U+00E9) kullanırken, sonradan düz eyi ayrı bir birleşen aksan kod noktasıyla (U+0301) izliyor; ikisi görsel olarak aynı işareti üretiyor ama farklı sayıda kod noktasıyla. Bu yüzden onceden dört rune, sonradan beş rune sayıyor, ve == ile karşılaştırıldıklarında eşit çıkmıyorlar — dizgi karşılaştırması bayt bayt yapılıyor, görsel eşdeğerliğe bakmıyor. Diziler ve Dilimler dersinde ölçülen == işlecinin dizide içeriğe baktığı kuralı burada da geçerli: dizgi karşılaştırması da içeriğin (baytların) birebir eşitliğine bakıyor, ekranda nasıl göründüğüne değil. İki dizgi görsel olarak ayırt edilemez olsa bile, farklı kod noktası dizilimine sahiplerse == bunları farklı sayıyor. Bu, kullanıcı girdisiyle çalışan bir kodun karşılaşabileceği somut bir sorun: aynı görünen iki metin, kaynakları farklı olduğunda (biri klavyeden, biri bir dosyadan, biri başka bir sistemden gelmiş olabilir) == ile karşılaştırıldığında eşit çıkmayabiliyor — hatanın nedeni karşılaştırma işlecinde değil, iki girdinin baştan farklı kod noktalarıyla kodlanmış olmasında.

Bu sınır, “görünen işaret” birimine dilin kendisinde bir karşılık olmadığını gösteriyor. Bilgisayarlar Nasıl Çalışır dersinde kapatılan grafem kavramı tam olarak bunu adlandırıyor: bir ya da daha çok kod noktasının bir araya gelip kullanıcının tek bir işaret olarak gördüğü şeyi oluşturması. Go’nun standart kitaplığı rune sayma dışında bir araç sunmuyor; grafem sayımı, kod noktalarını birleştirme kurallarını uygulayan ayrı bir adım gerektiriyor ve bu adım bu ölçümün kapsamı dışında.

Üç ölçü birimi böylece üç ayrı soruya yanıt veriyor ve hiçbiri ötekinin yerini tutmuyor. Bayt sayısı, bir dizginin bellekte ya da bir ağ üzerinden ne kadar yer kapladığını söylüyor — depolama ve aktarım burada önemli. Rune sayısı, kaç kod noktasının art arda geldiğini söylüyor — dilin kendi tip sisteminin sınırı burada. Grafem sayısı ise bir kullanıcının ekranda kaç “işaret” gördüğünü söylüyor, ama bu bilgi dilin tip sisteminde hiç durmuyor; bir dizginin kendisine bakarak elde edilemiyor. lenin bayt saydığını bilmeyen bir kod bir dizgiyi ortadan kesip geçersiz bir UTF-8 dizisi üretebilir; rune sayısının grafem sayısı olmadığını bilmeyen bir kod da “beş karakterle sınırla” gibi bir kuralı birleşik işaretli metinlerde yanlış uygulayabilir. İkisi de aynı kökten geliyor: dizginin kendi ölçü birimini, kullanıcının gördüğü ölçü birimiyle karıştırmak.

Özet

  • Bir Go dizgisi bir bayt dizisidir: len bayt sayar, indeksleme bir bayt döndürür — Sıfır Değerler dersindeki açıkta kalan üç durumdan biri buydu.
  • Dizgi değiştirilemez; bir bayta yazmak derleme zamanında reddedilir. “Değiştirme” her zaman yeni bir dizgi üretmek anlamına gelir; []byte dönüşümü bile orijinali paylaşmaz, bağımsız bir kopya çıkarır.
  • range ile gezinmek bayt değil rune verir; bayt indeksleri çok baytlı kod noktalarında ardışık olmayabilir.
  • Rune sayısı bayt sayısından daha güvenilir bir “karakter” ölçüsü gibi görünse de, görünüşte aynı metin birleşik ya da ayrı kod noktalarıyla farklı rune sayıları üretebilir — “karakter” dediğimiz şeyin dilde doğrudan bir karşılığı yok, grafem kavramı bu boşluğu adlandırıyor.

Sonraki Adım

Bu konu, dizi ile başlayıp dilim, sözlük, yapı, işaretçi ve son olarak dizgiyle kapandı; her biri kendi yazılmayan sorusunu farklı bir yönde yanıtladı — kimi kopyaladı (dizi, yapı), kimi paylaştı (dilim, sözlük, işaretçi), kimi hiç yazılamadı (dizgi). Altı dersin ortak sorusu hep aynıydı: bir değeri bir yere aktardığında ne kopyalanır, ne paylaşılır? Bir dizginin ne olduğunu dilin tip sistemi söylüyor: bir bayt dizisi, belirli bir kodlamayla, değiştirilemez bir değer. Ama bir dizginin ne anlama geldiğini — kod noktalarının hangi görünen işareti oluşturduğunu, bir adın ne temsil ettiğini — tip sistemi söylemiyor.

Sıradaki konu aynı soruyu kaynağın dışına taşıyor. Şimdiye kadar ölçülen her şey bir tek dosyanın, bir tek paketin içindeydi — bütün örnekler package main içinde, tek bir dizinde durdu; hiçbir ölçüm bir adın başka bir dizindeki koddan görünüp görünmeyeceğini sormadı. Ama gerçek bir program tek bir dosyadan ibaret değil: birden çok dosyaya, birden çok pakete, birden çok bağımlılığa yayılıyor. Bir adın başka bir dosyadan görülüp görülemeyeceğini, bir bağımlılığın hangi sürümünün seçileceğini, ve yazılmamış bir biçim kuralının nasıl uygulanacağını kim belirliyor? Bu sorular artık dilin tip sisteminin değil, paket ve modül düzeneğinin sorularıdır.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat