Thread overview
Tek boyutlu diziyi iki boyutlu olarak kullanmak
Mar 13, 2022
Ali Çehreli
Mar 13, 2022
Salih Dincer
Mar 18, 2022
Salih Dincer
Mar 20, 2022
Salih Dincer
Mar 17, 2022
Salih Dincer
Jul 19, 2022
Salih Dincer
Aug 08, 2022
Ali Çehreli
March 13, 2022
C ve C++'ta da olmadığı gibi, D'de de çok boyutlu dizi yoktur. (Yani, dilin kendisinde böyle bir kavram yok.) Elemanların türü dizi olunca, yani "dizi dizisi" olunca zaten çok boyutlu dizi haline geliyor:

import std.stdio;

// İki boyutlu olarak kullanılan bir int dizisi döndürür
int[][] diziDizisi(size_t satırAdedi, size_t satırUzunluğu) {
  int[][] sonuç;

  foreach (i; 0 .. satırAdedi) {
    int[] satır;
    satır.length = satırUzunluğu;

    // Yukarıdaki iki satır yerine şu da olur:
    // int[] satır = new int[satırUzunluğu];

    sonuç ~= satır;
  }

  return sonuç;
}

void main() {
  auto dizi = diziDizisi(3, 10);

  writeln(dizi);
}

Çıktısı, üç tane dizi elemanı olan bir dizidir:

[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]

Önce altını çiziyorum: Aşağıdaki anlatacaklarımın %99.99 programı etkilemez. :) Kodumuzu yukarıdaki gibi yazarız ve program hiçbir hız sorunu göstermeden mutlu mutlu çalışır. :)

Yine de, yukarıdaki kodun bazı sakıncaları vardır ve bunlar bazı durumlarda ölçülebilir hız kaybına neden olabilir:

1) Hem satırları tutan dizi, hem de satırlar dinamik bellekten ayrılıyorlar. Yani, yukarıdaki kodda en az (satırAdedi + 1) kere bellek ayrılır. (Ayrılan belleğin büyüklüğünden bağımsız olarak, salt bellek ayırma işlemi bile ölçülebilir derecede yavaş olabilir.)

2) Bellekler farklı adımlarla ayrıldıklarından, yukarıdaki dizinin bütün elemanları bellekte birbirlerinden uzak yerlerde duruyor olabilirler. Bu da modern mikroişlemcilerin belleği hızlı kullanma olanaklarından yararlanma şansını azaltır.

Durum gerçekten böyle olduğunda, çok eskiden beri bilinen bir yöntem, belleği tek parçada ayırmak ama elemanlar iki boyutlu olarak erişmektir:

import std.stdio;

// Bu sefer tek seferde ayrılmış olan bellek döndürür.
// Yani, dönüş değeri tek boyutlu dizidir.
int[] diziDizisi(size_t satırAdedi, size_t satırUzunluğu) {
  int[] sonuç = new int[satırAdedi * satırUzunluğu];
  return sonuç;
}

// Bunu bir sonraki programda kolaylaştıracağız.
// Durum bu haliyle çok kötü çünkü dizinin satır uzunluğunu da
// her seferinde vermek zorunda kalıyoruz.
ref eleman(int[] dizi, size_t satırUzunluğu, size_t satır, size_t sütun) {
  size_t buSatırınBaşı = satır * satırUzunluğu;
  size_t elemanınYeri = buSatırınBaşı + sütun;

  return dizi[elemanınYeri];
}

void main() {
  size_t satırAdedi = 3;
  size_t satırUzunluğu = 10;

  auto dizi = diziDizisi(satırAdedi, satırUzunluğu);

  foreach (satır; 0 .. satırAdedi) {
    foreach (sütun; 0 .. satırUzunluğu) {
      import std.conv;
      int değer = (satır * 1000 + sütun).to!int;
      eleman(dizi, satırUzunluğu, satır, sütun) = değer;
    }
  }

  writeln(dizi);
}

Programda da belirtildiği gibi, böyle bir diziyi kendisiyle çok ilgili olan satırUzunluğu kavramında ayrı tutmak, hem kullanışsızdır hem de hataya açıktır. İşte böyle durumlarda aklımıza struct geliyor. Hatta, diziDizisi() gibi bir "kurma" işlevi yerine struct'ın kurucu işlevini (this()) kullanabiliriz:

import std.stdio;

struct Dizi {
  int[] elemanlar;
  size_t satırUzunluğu;

  this(size_t satırAdedi, size_t satırUzunluğu) {
    this.elemanlar = new int[satırAdedi * satırUzunluğu];
    this.satırUzunluğu = satırUzunluğu;
  }

  // Önceki 'elemanlar' ve 'satırUzunluğu' değişkenleri
  // zaten artık üye değişkenler...
  ref eleman(size_t satır, size_t sütun) {
    size_t buSatırınBaşı = satır * satırUzunluğu;
    size_t elemanınYeri = buSatırınBaşı + sütun;

    return elemanlar[elemanınYeri];
  }
}

void main() {
  size_t satırAdedi = 3;
  size_t satırUzunluğu = 10;

  auto dizi = Dizi(satırAdedi, satırUzunluğu);

  foreach (satır; 0 .. satırAdedi) {
    foreach (sütun; 0 .. satırUzunluğu) {
      import std.conv;
      int değer = (satır * 1000 + sütun).to!int;
      dizi.eleman(satır, sütun) = değer;
    }
  }

  writeln(dizi);
}

Tabii, eleman() gibi bir işlev yerine opIndex() ile işleç yükleme olanağından da yararlanabilirdik. (Öğretmen bu konuyu ödev olarak bırakıyor. :p)

Konu burada bitmiyor.

Yukarıdaki program, dizi satır satır işlenecekmiş gibi yazılmış. Yani, tek boyutlu dizinin elemanları "önce ilk satır, arkasından ikinci satır, vs." olarak yerleştiriliyor. Mikroişlemcinin belleği önceden hızlıca okuyan düzeneği (prefetch), biz satırdaki elemanlara art arda erişirken yararlı olacaktır. (O düzenek bellekte ileriye doğru ilerlendiğini algılar ve sonraki baytları da okur.) Güzel...

Ancak, eğer elemanlara "önce ilk sütun, sonra ikinci sütun, vs." diye erişeceksek mikroişlemcinin düzeneğinin hiçbir yararı olmaz. Hmmm. Bu durumda belki de tek boyutlu diziyi şöyle tasarlamalıymışız: önce ilk sütun değerleri yan yana, sonra ikinci sütun değerleri, vs. Bu da çok kolay:

1) satırUzunluğu yerine satırAdedi değişkenini tutmak

2) eleman() işlevini şöyle değiştirmek:

  ref eleman(size_t satır, size_t sütun) {
    size_t buSatırınBaşı = sütun * satırAdedi;
    size_t elemanınYeri = buSatırınBaşı + satır;

    return elemanlar[elemanınYeri];
  }

Bu son değindiğim konu, tek boyutlu dizinin "row-major" mı yoksa "column-major" mı olduğudur. (mir belgelerinde de geçiyor.)

Ali
March 13, 2022

On Sunday, 13 March 2022 at 18:01:55 UTC, Ali Çehreli wrote:

>

[...]

  1. Bellekler farklı adımlarla ayrıldıklarından, yukarıdaki dizinin bütün elemanları bellekte birbirlerinden uzak yerlerde duruyor olabilirler. Bu da modern mikroişlemcilerin belleği hızlı kullanma olanaklarından yararlanma şansını azaltır.

Durum gerçekten böyle olduğunda, çok eskiden beri bilinen bir yöntem, belleği tek parçada ayırmak ama elemanlar iki boyutlu olarak erişmektir:

Yani şunu demek istiyorsunuz:

import std.stdio;

auto diziDizisi(T)(size_t x, size_t y) {
  T[][] sonuç;

  ubyte[] arr = new ubyte[x * y * T.sizeof];
  size_t m = y * T.sizeof;
  foreach (i; 0 .. x) {
    size_t n = i * m;
    sonuç ~= cast(T[])arr[n .. n + m];
  }
  return sonuç;
}

alias Tür = int;

void main()
{
  auto dizi = 3.diziDizisi!Tür(4);
  foreach(x, ref n; dizi)
    foreach(y, ref m; n)
      m = cast(Tür)(x * n.length + y);

  dizi.writefln!"%(%s, %)";

  Tür* ilk = &dizi[0][0];
  foreach(n; 0..12) {
    auto adr = ilk + n;
    writefln!"%s: %s"(*adr, adr);
  }
} /* ÇIKTISI:
[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]
0: 7F2E7D0D4000
1: 7F2E7D0D4004
2: 7F2E7D0D4008
3: 7F2E7D0D400C
4: 7F2E7D0D4010
5: 7F2E7D0D4014
6: 7F2E7D0D4018
7: 7F2E7D0D401C
8: 7F2E7D0D4020
9: 7F2E7D0D4024
10: 7F2E7D0D4028
11: 7F2E7D0D402C
*/
March 17, 2022

On Sunday, 13 March 2022 at 18:01:55 UTC, Ali Çehreli wrote:

>

Tabii, eleman() gibi bir işlev yerine opIndex() ile işleç yükleme olanağından da yararlanabilirdik. (Öğretmen bu konuyu ödev olarak bırakıyor. :p)

Konu burada bitmiyor.

Bu konu biter mi hocam, her yerde karşımıza çıkıyor. Şimdi bir taşla birkaç kuş daha vurma zamanı. Hem ödevimi (opIndex'i) de yapabilirim.

İzninizle böyle güzel bir konudan, çoğumuz için başbelası (be the devil) olan ama bir o kadar da zevkli matematiğe girmek istiyorum...

Tam da matematik (Calculus, Linear al-Gebra) çalıştığım şu dönemde array, matrix gibi konular karşıma çıkınca, geometride 2 çizginin kesişmesi ve oradan oyun programlama falan deli şeyler aklımdan geçiyor. Baştan başlayayım:

Şimdi anlatacaklarım çok temel olsa da üzerinden geçmek zorundayız:

İki boyutlu uzay da bir O noktası hayal edin ve orada x/y eksenlerinde birbiriyle kesişen 2 çizgimiz olsun. Lineer denklemleri örneğin şöyle olabilir:

y = 2x -3 => -2x +y = -3
y = -3x +1 => 3x +y = 1

  int[3][2] denklemler = [[ -2, 1, -3 ],
                          [  3, 1,  1 ]];

Yukardaki dizi ise matematikte 2D matris karşılığı ama dikkat, [ 1 ] şeklinde de yazılır ve kural olarak matris anılırdı, hatırlayın. Neyse, aynı şekilde bu çizgilerin koordinatları uzaya verelim:

import std.stdio;

void main()
{
  auto çizgiler = Uzay2D!int(denklemler);

/*
 * Normalde float/double çalışmak icap eder hatta 3D ile birlikte
 * complex sayılara girmek falan ama hala tam hakim değilim o konulara.
 * Şimdi ortaokuldan öğrendiğimiz x/y düzlemini programlayalım.
 * Bakalım yeni bir özel başlığa (belki oyun içinde göstermeye)
 * gücümüz yetecek mi?
 */

  with(çizgiler)
  {
    version(1)
    {
      z(çizgiler[0]).write(", ");
      x(çizgiler[1]).write();

    } else {

      çizgi[0].z.write(", ");
      çizgi[1].x.write();
    }
  }
  writeln; /* -3, 3
  /             ^--- Evet, evet! Bütün çıktı bu :)
}

/*
 * 2 versiyon aklıma geldi, ilki kolaya kaçmaktı ama içler acısıydı.
 * Çünkü denklemleri öyle uzay sal gitsin olmuyor ve paketlemek (Vect3D)
 * lazım geliyor. O yüzden //version = 1; satırını açarsanız diğerini de
 * denemeniz mümkün:
 */

//version = 1;
version(1)
{

  struct Uzay2D(T) {
    T[3][2] matris;

    T x(T[3] çizgi) { return çizgi[0]; }
    T y(T[3] çizgi) { return çizgi[1]; }
    T z(T[3] çizgi) { return çizgi[2]; }

    T[3] opIndex(size_t index) {
      return matris[index];
    }
  }

} else {

  struct Uzay2D(T) {
    Vect3D[2] çizgi;

    struct Vect3D { T x, y, z; }

    this(T[3][2] veri) {
      foreach(i, v; veri) {
        çizgi[i] = Vect3D(v[0], v[1], v[2]);
      }
    }
  }
}

Farketmişsinizdir, matris isimli elemanı bulunan ilk versiyonda x/y/z için gereksiz yere 3 işlev yapma ihtiyacı hissettim ama kullanımı opIndex'e rağmen hiç pratik değil, with() falan da kurtarmıyor...:)

Ancak sonraki sürüm gayet iyi çünkü 2. bir yapı her şeyi düzene sokuyor ve ekstra (x/y/z) bir şey tanımlamadan adeta tereyağından kıl çeker gibi. İşte İsmail Emre için çok güzel bir örnek. Çünkü bir şeyleri yapılar içinde düzene sokunca onları yönetmek/erişmek çok kolay oluyor.

Bu arada küçük bir açıklama: x/y/z olarak tanımladığım henüz 3D değil. Yani z, aslında eşitliğin diğer tarafındaki değer. Henüz 3. boyuta girmedik ve sadece bunlar bir denemedir. Devamı gelecek inşaallah.

Başarılar...

March 18, 2022

On Sunday, 13 March 2022 at 20:14:17 UTC, Salih Dincer wrote:

>
import std.stdio;

auto diziDizisi(T)(size_t x, size_t y) {
  T[][] sonuç;

  ubyte[] arr = new ubyte[x * y * T.sizeof];
  size_t m = y * T.sizeof;
  foreach (i; 0 .. x) {
    size_t n = i * m;
    sonuç ~= cast(T[])arr[n .. n + m];
  }
  return sonuç;
}

Yukardaki işlev, sıralı depolanmış T[süt][sat] dizisi döndürür. Bu buffer tekniğini, yeni açılan şu başlıkta öğrenmiştim: https://forum.dlang.org/post/ysfyqeuwbsevlfcungzk@forum.dlang.org

Ama dikkat, biz açık bir şekilde dizi tanımlarken İzmir usulü parametreler (plaka kodu 35) kullanırdık. İşlevde ise (5, 3) şeklinde, yani Rize usulü olacak 😀

      enum {
        sat = 5, // satır, dizi içindeki DİZİ
        süt = 3  // sütun, DİZİ içindeki VERİ
      }
      auto dizi = diziDizisi!int(sat, süt);

Eminim, programlamaya yeni başlayan arkadaşlarımız bu durumu sonradan fark edecek. Bu iki önemli ayrıntıyı belirtmezsem şu kandil gecesi huzurlu olmayacaktım!

Dip Not: Bir de yeni öğrendiğim ikinci bir konu bonus olsun. Yukarda Uzay2D kurucusu gelen parametreyi açık bir şekilde auto olmadan kurulsun istiyor. Sanırım sebebi statik dizi (yani köşeli parantez içi parametreli) kurmam. Ne yaptıysam auto denklemler = ... şekelinde kurduğum diziyi parametre olarak kabul ettiremedim. Çünkü derleyici, auto olarak kurulan diziyi dinamik şekilde kurar. Burda Ali hocanın vesile olduğu diziDizisi() harika bir tool, teşekkürler.

Şimdi tamam oldu, iyi kandiller...

March 20, 2022

On Friday, 18 March 2022 at 01:21:06 UTC, Salih Dincer wrote:

>

On Sunday, 13 March 2022 at 20:14:17 UTC, Salih Dincer wrote:
Şimdi...

Belli ki bu konuya kimsenin ilgisi kalmamış :)

Ben eski Pascal kodlarına kadar girdim; orada ne yapıyorduk diye. Bunda 30 sene önce ilk başladığımda, uzun bir süre (windows dahil) Pascal'da kod yazıyordum. Meğer D'ye benzer yanları varmış. Sonra Delphi çıktı ve hala var galiba. Borland ne oldu, kim bayrağı taşıyor bilmiyorum!

Aşağıdaki hatıra olsun...

uses crt;

var
   sayi1:array[1..5, 1..6] of integer;
   sayi2:array[1..5, 1..6] of integer;
  toplam:array[1..5, 1..6] of integer;
  i, j : byte;

begin
  clrscr;
  randomize;

  for i:=1 to 5 do begin
    for j:=1 to 6 do begin
       sayi1[i, j] := random(100);
       sayi2[i, j] := random(100);
      toplam[i, j] := sayi1[i, j] + sayi2[i, j];
      write(toplam[i, j] : 10);
    end;
    writeln();
  end;
end. /* ÇIKTISI (random tabi!):
 Output

       152       103       143       169        86       126
       169       112        31        90       141       123
        46        26       110        26       143       140
        81        64        62       176        90        41
        96        72        66        85       110        42
*/
July 19, 2022

On Sunday, 13 March 2022 at 18:01:55 UTC, Ali Çehreli wrote:

>
struct Dizi {
  int[] elemanlar;
  size_t satırUzunluğu;

  this(size_t satırAdedi, size_t satırUzunluğu) {
    this.elemanlar = new int[satırAdedi * satırUzunluğu];
    this.satırUzunluğu = satırUzunluğu;
  }

  // Önceki 'elemanlar' ve 'satırUzunluğu' değişkenleri
  // zaten artık üye değişkenler...
  ref eleman(size_t satır, size_t sütun) {
    size_t buSatırınBaşı = satır * satırUzunluğu;
    size_t elemanınYeri = buSatırınBaşı + sütun;

    return elemanlar[elemanınYeri];
  }
}

Tabii, eleman() gibi bir işlev yerine opIndex() ile işleç yükleme olanağından da yararlanabilirdik. (Öğretmen bu konuyu ödev olarak bırakıyor. :p)

Bu ödev basitti çünkü tek yaptığım işlevin ismini ref T opIndex() olarak değiştirmekti. Bitmedi, başka değişikliğe de ihtiyaç olabilirdi...

Önce yapıya İngilizce ARRAYish anlamı vermek istedim. Çünkü bu bildiğimiz diziden farklı sonuçlar verecek. Ek olarak başka türlerle kurulabilmeliydi:

import std.stdio;

struct DiziGibi(T) {
  private { // bunun nedenini sonra açıklayacağım...
    T[] elemanlar;
    const size_t satırAdedi, satırUzunluğu;
  }

  this(size_t satırAdedi, size_t satırUzunluğu) {
    this.elemanlar = new T[satırAdedi * satırUzunluğu];
    this.satırAdedi = satırAdedi;
    this.satırUzunluğu = satırUzunluğu;
  }

  ref T opIndex(size_t satır, size_t sütun) {
    size_t buSatırınBaşı = satır * satırUzunluğu;
    size_t elemanınYeri = buSatırınBaşı + sütun;

    return elemanlar[elemanınYeri];
  }
  // peki, gerçek bir dinamik diziye dönüşmesini istesek?

  auto opCast(R : T[][])() {
    T[][] dizi;
    foreach(i; 0..satırAdedi)
    {
      size_t n = i * satırUzunluğu;
      dizi ~= elemanlar[n..n + satırUzunluğu];
    }
    return dizi;
  }
  // son olarak iki de özellik ekleyelim...

  @property {
    auto dup() {  // veriyi başka adreslere kopyalasın
      T[][] dizi;
      foreach(dilim; cast(T[][])this)
         dizi ~= dilim.dup;
      return dizi;
    }

    auto length() { // uzunluğu pratik olarak hesaplasın
      return satırAdedi * satırUzunluğu;
    }
  }
}

// Hadi deneyelim 😀

alias Tür = int;

enum : size_t {
  Sat = 5,     // row: Satır
  Süt = 3,  // column: Sütun
  Top = Sat * Süt
}

void main()
{
  Tür[Süt][Sat] çizgiler = [ [ -3, -2, -1 ],
                             [  0,  1, -2 ],
                             [  3,  4,  5 ],
                             [  6,  7,  8 ],
                             [  9, 10, 11 ]
                           ];

  auto dizi = diziDizisi!(Tür, true)(Sat, Süt, 10);
  dizi.writefln!"%(%s\n%)";

  auto arrayish = DiziGibi!Tür(Sat, Süt);
  foreach(r; 0..Sat)        // row: Satır
    foreach(c; 0..Süt) { // column: Sütun
      arrayish[r, c] = çizgiler[r][c];
    } // copy array

  dizi = cast(Tür[][])arrayish;

  auto diziKopyası = arrayish.dup;
  foreach(r; 0..Sat)        // row: Satır
    foreach(c; 0..Süt) { // column: Sütun
      assert(&dizi[r][c] != &diziKopyası[r][c]);
    } // not equal
  dizi.writefln!"%(%s\n%)";
}

Kafanız karışmamıştır umarım? Çok bir şey yapmadım:

1. Yükleme(opIndex):

Bunu zaten Ali hocam yapmıştı ve bize ödev olarak bırakmıştı. Tek yaptığı dizilerdeki köşeli parantezi kullanabilmek. Ama dikkat çift boyutlu olduğu için gerçek bir dizi gibi görünmeyecek ve indeks araları virgül ile ayrılması gerekecek.

2. Yükleme(opCast):

Burda da pek bir şey yok çünkü dilimleri bir dizi halinde döndürüyor. Hatta fazladan satırAdedi'ne ihtiyacım oldu. Güzel olan aynı adresleri paylaşması ama DiziGibi() nesnesiyle işimiz bitince n'olcak? Cevabını bilmiyorum çünkü denemedim. Tabi belleğe başka kopyasını alabilsek iyi olabilir!

1. Özellik(.dup):

Alın size bildik kopya özelliği! Aslında bu da opCast()'dekine benzer şekilde dilimlerden oluşuyor ama ekstradan dilin dup özelliğini kullandım. Yani aynı şeyler, hiçbir esprisi yok...

2. Özellik(.length):

Bu belki gereksizdi ama elemanlar dizisinden elde etmek istemedim. Doğrudan hesaplanabilir bir özellik belki daha iyidir diye düşündüm.

Son olarak private alma sebebim; her ne kadar aynı modülde bir esprisi olmasa da dışarıdan diziye eleman eklenmemeli ve kurulurken satır/sütun uzunlukları değiştirilememeli yoksa birbirlerine göre tutarsız olacak. Neyse ki const kısmen imdadımıza yetişti (Hayret! Hayatımda ilk defa const bir işe yaradı 😀) ve kısmen ek bir write protect gerçekleştirdim.

Tabi bunu diziye yapamıyoruz çünkü elemanlara opIndex() ile erişiyoruz. Bize string'deki gibi immutable özelliği gibi bir şey lazım ama bunun nasıl yapılabileceğini bilmiyorum.

Dip Not: Kod içindeki diziDizisi!(T, size_t)'nin son sürümünü şu başlıkta bulmanız (şu an v2) mümkün. Ama çizgiler[][] ve diğer veriler tamamen uydurmaca olduğu için denemeleri, kendi (belki iota ile oluşturulan) veri setinizi kullanarak yapabilirsiniz.

Başarılar...

August 08, 2022
On 7/19/22 14:58, Salih Dincer wrote:

> struct DiziGibi(T) {

Belki artık Matris de denebilir. :)

>      auto dup() {  // veriyi başka adreslere kopyalasın
>        T[][] dizi;
>        foreach(dilim; cast(T[][])this)
>           dizi ~= dilim.dup;
>        return dizi;
>      }

Ben benzer bir kodu bir sunumumda göstermiştim:

```
  arr = strings[0..length].map!(s => s.fromStringz.idup).array;
                                           //       ↑      ↑
                                           //   copies   allocates

// Note: As an optimization exercise, all D strings as well as the D
         array can be inside a single memory block.
```

Notta belirttiğim gibi, her şey için tek bellek ayrılabilir ve belki kod biraz daha hızlı işleyebilir.

Şuradaki sunumun "57 / 112"nci adımında:

  https://dconf.org/2020/online/#ali2

>      auto length() { // uzunluğu pratik olarak hesaplasın
>        return satırAdedi * satırUzunluğu;
>      }
>    }
> }

> Bu belki gereksizdi ama `elemanlar` dizisinden elde etmek istemedim.
> Doğrudan hesaplanabilir bir özellik belki daha iyidir diye düşündüm.

Haklısın. Hatta bir keresinde ben alt dizilerin length'lerini toplayarak da döndürüyordum. (Benim durumumda tek satır uzunluğu kavramı yoktu.) Ama dikkat edilmesi gereken bir konu, alt dizilerin sayıları fazla olduğundan .length artık beklendiği gibi O(1) değil, O(M) olarak çalışır (M, alt dizi sayısı). Yani, bir kaç tane alt dizi olduğunda tamam ama fazlaysa kötü. :)

> Bize string'deki gibi immutable özelliği gibi bir şey lazım
> ama bunun nasıl yapılabileceğini bilmiyorum.

Elemanların immutable olarak gerçekleştirilmeleri çok zor olabilir. cast vs. kullanmamız gerekebilir ve aslında bizi undefined behavior durumlarına sokar. (Bildiğim kadarıyla, dil bu konuda fazla sıkı olarak görülüyor.)

Ali