Anasayfa / C# / String.Create Metodu Nasıl Kullanılır?

String.Create Metodu Nasıl Kullanılır?

Kod yazarken dikkat etmemiz gereken en önemli noktalardan biri gereksiz memory kullanımından kaçınmak. Yanlış memory kullanımı dediğimizde aklımıza ilk gelen tiplerden biri de string tipi. String yapısı gereği immuatable bir tip olduğu için string üzerinde değişiklik yapmak istediğimizde, farklı stringleri birleştirmek istediğimizde vs.. yeni bir string yaratmamız gerekiyor. Daha öncesinde stringleri belirli bölgesinden kesmek istediğimizde de Substring metodunu kullanarak yeni bir stringin yaratılmasına sebep olabiliyorduk. Ancak yeni gelen StringSegment veya Span tipleriyle yeni string yaratılmasından da kaçınmamız şu an mümkün.

Runtime esnasında elimizdeki belirli stringleri birleştirip yeni bir string yaratmak istediğimizde ve bu işlemi optimize bir şekilde yapmak istediğimizde kullanmamız gereken tip StringBuilder. Ancak StringBuilder string birleştirme operasyonlarını optimize bir şekilde yapsa da arka planda birleştirilecek olan stringleri bir bufferda sakladığını için ekstra heap allocationa neden olmakta. Bu buffer eklediğimiz stringlerin boyutuna göre de yerine göre resize edilmekte ve değerler yeni buffera kopyalanmakta. Bu da tabi ki performans kritik senaryolarda bir yük getirmekte. Bunu optimize etmek için eğer oluşturulacak olan stringin final uzunluğu biliniyorsa StringBuilder yaratılırken bir capacity belirtmek bizi en azından bufferın arka planda resize edilmesinden kurtaracaktır.

Bunun yanında özellikle sık çalışan kodlarda sürekli olarak StringBuilder nesnesi yaratmak arka planda aynı zamanda yeni buffer yaratılmasına neden olacaktır. Bu nedenle sürekli olarak buffer yaratılıp sonra GC tarafından temizlenmesinin önüne geçmek için StringBuilderlar bir object pool içerisinde saklanıp gerektiği zaman pooldan alınıp iş bittiği zaman poola geri bırakılabilir(Object Pooling).

.NET Core 2.1 ile beraber string tipinin içerisine eklenen Create metoduyla arka planda buffer allocationa neden olmadan da etkin bir şekilde string yaratmak mümkün hale geldi. Bu metodu kullanırken de göreceğiniz üzere string parametre olarak geçtiğimiz delegate içerisinde mutable durumda. Create metodunu kullanabilmemiz için ilk şart oluşacak olan stringin uzunluğunu önceden biliyor olmak. Eğer önceden bilmiyorsak veya hesaplayamıyorsak bu metodu kullanmamız mümkün değil.

public static string Create<TState>(int length, TState state, System.Buffers.SpanAction<char, TState> action);

Create metodunun parametrelerine bakarsak,

  • length: Oluşturulacak stringin uzunluğu.
  • state: stringi yaratırken kullanacağımız ve delegate’e parametre olarak gelecek olan object.(Eğer birden fazla değişkeni parametre geçmemiz gerekirse tuple kullanabiliriz.)
  • action: string yaratılırken çağrılacak olan delegate.

Bir kullanım örneği;

List<string> list = new List<string>()
{
    "test",
    "test2"
};
 
var str3 = string.Create(9, list, (c, state) =>
{
    int index = 0;
    state[0].AsSpan().CopyTo(c);
    index += state[0].Length;
    state[1].AsSpan().CopyTo(c.Slice(index));
});

Şimdi diyelim ki elimizde bir liste var ve bu listedeki her bir elemanı birleştirip string yaratmak istiyoruz. İlk kullanım örneği olduğu için bazı değerleri statik yaparak ilerliyoruz. Liste iki elemanlı olduğu için ve kod yazarken içerisindeki değerleri görebildiğimiz için oluşacak olan stringin final uzunluğunu tahmin etmemiz mümkün. Bu nedenle ilk parametreyi 9 olarak veriyoruz. İkinci parametre ise stringi yaratırken kullanacağımız state nesnesi. Bunun için yukarıda tanımlanan list’i parametre olarak geçiyoruz.

Son parametre olarak da stringi initialize eden delegate’i veriyoruz. Bu delegate’in ilk parametresi stringin arkasında saklanan char arrayi temsil eden bir Span. Bu parametreyi kullanarak stringi initialize edebileceğiz. İkinci parametre ise state parametresinin kendisi. Biz bu değişkeni zaten ikinci parametre olarak geçmiştik diyebilirsiniz. Ancak biz delegate içerisinde list değişkenini kullanırsak bu yeni bir allocationa neden olacağı için efektif olmayacaktır. Bu nedenle stringi yaratırken mutlaka state parametresini kullanarak ilerlememizde fayda var.

Delegate’in içeriğine baktığımızda ise ilk olarak listenin ilk elemanını kopyalıyoruz sonrasında ise ikinci elemanı kopyalıyoruz. Böylece baktığımızda stringleri tutmak için arkada bir buffer allocate etmeye gerek kalmıyor.

Daha generic çalışan bir implementasyon yaparsak.

List<string> list = new List<string>()
{
    "test",
    "test2"
};
var length = 0;
for (int i = 0; i < list.Count; i++)
{
    length += list[i].Length;
}
 
var str3 = string.Create(length, list, (c, state) =>
{
    int index = 0;
    for (int i = 0; i < state.Count; i++)
    {
        state[i].AsSpan().CopyTo(c.Slice(index));
        index += state[i].Length;
    }
});

Şimdi de son olarak ufak bir benchmarking yapalım ve aradaki farkı inceleyelim.

[MemoryDiagnoser]
[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
[MarkdownExporter]
public class Benchmark
{
    List<string> list;
 
    [Params(10, 100, 400)]
    public int Size { get; set; }
 
    [IterationSetup]
    public void Setup()
    {
        list = new List<string>();
        for (int i = 0; i < Size; i++)
            list.Add("Test");
    }
 
    [Benchmark]
    public string StringCreate()
    {
        var length = 0;
        for (int i = 0; i < list.Count; i++)
        {
            length += list[i].Length;
        }
        return string.Create(length, list, (c, state) =>
            {
                int index = 0;
                for (int i = 0; i < state.Count; i++)
                {
                    state[i].AsSpan().CopyTo(c.Slice(index));
                    index += state[i].Length;
                }
            });
    }
 
    [Benchmark]
    public string StringBuilderDefault()
    {
        var builder = new StringBuilder();
        for (int i = 0; i < list.Count; i++)
        {
            builder.Append(list[i]);
        }
        return builder.ToString();
    }
 
    [Benchmark]
    public string StringBuilderInitialCapacity()
    {
        var capacity = 0;
        for (int i = 0; i < list.Count; i++)
        {
            capacity += list[i].Length;
        }
        var builder = new StringBuilder(capacity);
        for (int i = 0; i < list.Count; i++)
        {
            builder.Append(list[i]);
        }
        return builder.ToString();
    }
}

Çıktı;

Method Size Mean Error StdDev Median Gen 0 Gen 1 Gen 2 Allocated
StringCreate 10 1.028 μs 0.0303 μs 0.0839 μs 1.000 μs 104 B
StringBuilderDefault 10 2.333 μs 0.3873 μs 1.1420 μs 1.600 μs 448 B
StringBuilderInitialCapacity 10 1.273 μs 0.0291 μs 0.0574 μs 1.300 μs 256 B
StringCreate 100 3.152 μs 0.4089 μs 1.2056 μs 3.500 μs 824 B
StringBuilderDefault 100 2.385 μs 0.0512 μs 0.0718 μs 2.400 μs 2280 B
StringBuilderInitialCapacity 100 3.216 μs 0.4154 μs 1.2116 μs 2.400 μs 1696 B
StringCreate 400 3.224 μs 0.0647 μs 0.0664 μs 3.200 μs 3224 B
StringBuilderDefault 400 19.884 μs 1.4567 μs 4.2492 μs 20.500 μs 7896 B
StringBuilderInitialCapacity 400 5.924 μs 0.4355 μs 1.2773 μs 6.300 μs 6496 B

Gördüğümüz gibi Create metodu kullandığımızda hem kullanılan memory daha düşük hem de daha performanslı bir şekilde stringi yaratabiliyoruz. Performansla ilgili yazdığımız yazıda olduğu gibi burada da kullanımları kendi senaryolarınızla karşılaştırıp, benchmarking yapmanız ve ona göre karar vermeniz en doğrusu. Özellikle çok fazla çalışan ve string üretilen kodlarda kullanmak size büyük kazançlar sağlayabilir. Ancak çok sık çalışmayan yerlerde beklediğiniz faydayı da göremeyebilirsiniz.

“Her yerde her metodu değil ihtiyaç duyduğumuz doğru yer kullanmalıyız.”