Teknik Makale

Delphi Kurucu İstisnaları ve Nil-Güvenli Yıkıcılar

Bir Delphi kurucusu istisna fırlattığında RTL, hemen yıkıcıyı yarım kurulmuş örnek üzerinde çağırır; yani her yıkıcı, hiç atanmamış alanlarla baş etmek zorundadır. HotXLS bu kuralı kendi yıkıcılarında uygular, TXLSXRichText ve TXLSXCells dahil: o kurucuların içindeki bir ayırma başarısızlığı, kodunuza temizleme sırasında fırlatılan ikincil bir access violation olarak değil, orijinal EOutOfMemory olarak ulaşır

Hata sınıfı eski ve gözden kaçırması kolaydır. Bir servis, dolu bir sunucuda 300 MB'lık bir çalışma kitabını dönüştürür, bellek daralır ve günlükte bir yıkıcı içinde bir access violation görünür. O mesajın hiçbir yeri "bellek bitti" demez, sonra tekrar denemenin işe yarayacağını ima etmez ve yığın izi, kusursuz doğru görünen bir yıkıcıyı gösterir. Orijinal istisna birkaç mikrosaniye yaşadı ve sonra değiştirildi. Bu yazı, bunun neden olduğunu, HotXLS'nin yıkıcılarını bundan kaçınmak için nasıl kurduğunu ve kendi sınıflarınızın bunu hiç yapmaması için ne yazacağınızı adım adım geziyor

Bir Delphi kurucusu istisna fırlattığında ne olur?

Nesne, istisna çağıraya ulaşmadan önce sizin yerinize yok edilir ve yıkıcınız, kurucunun ulaşmış olduğu hangi durum varsa ona karşı çalışır. Dizi derleyici ve RTL tarafından sabittir. NewInstance örneği ayırır ve InitInstance sıfırla doldurur; böylece her nesne başvurusu nil, her tam sayı 0 ve her dizge boş başlar. Kurucu gövdesi, örtük bir istisna işleyicisinin içinde çalışır. O gövdede herhangi bir şey istisna fırlatırsa üretilen kod sanal yıkıcıyı çağırır: BeforeDestruction çalışır, sonra sizin Destroyunuz, sonra FreeInstance belleği serbest bırakır ve en sonunda istisna yeniden fırlatılır. AfterConstruction hiç çalışmaz ve X := TFoo.Create atamasının solundaki değişkene hiç atanmaz

Bir kurucu istisna fırlattığında RTL'nin, orijinal istisna çağıraya ulaşmadan önce yarım kurulmuş HotXLS örneğinde Destroy'u nasıl çağırdığını gösteren Delphi nesne yaşam döngüsü
Delphi kurucusu istisna fırlattığında RTL önce yarım kurulmuş örneği yok eder ve ancak ondan sonra orijinal istisnayı yeniden fırlatır; Destroy, kurmanın ulaştığı hangi durum varsa ona karşı çalışır

Yani yıkıcı, kurucusunun keyfi bir satırında durmuş bir nesne görür. Başarısızlıktan önce atanmış alanlar gerçek nesneler taşır. Sonrasındaki alanlar hâlâ nildir. Kurmanın bittiğini varsayarak yazılmış bir yıkıcı, o nil alanlardan birine başvurur ve kendi access violation'ını fırlatır

İkinci istisna asıl hasardır. Delphi bu temizlik sırasında fırlatılan istisnaları zincirlemez: access violation, EOutOfMemory'nin yerine geçer, orijinal istisna atılır ve çağıran yalnızca AV'yi görür. İşe yarayan teşhis, "bir ayırma başarısız oldu, daha çok bellekle yeniden dene", kaybolur; yerine bellek bozulmasına benzeyen bir şey geçer

İkincil access violation nereden geliyor?

Kurucusunun tamamlandığını varsayıp, çok ilk iç ayırma başarısız olduğunda nil olan bir liste alanına dokunan bir yıkıcıdan gelir. İki HotXLS sınıfı bu şekli net gösteriyor. Hem bağımsız TXLSXRichText.Create hem de yorum sahipli TXLSXRichText.Create(AOwner: TXLSXComment) aşırı yükü, inherited Create çağırıp sonra FRuns := TList.Create ayırır. TXLSXCells.Create(AOwnerSheet: TXLSXWorksheet), paket dizge tablolarını ve paket hücre depolamasını kurmadan önce aynı işi FList := TList.Create ile yapar. Lib/lxHandleX.pas içinde iki yıkıcı o alanları korumalıyor:

procedure TXLSXRichText.ClearCore;
var
  i: Integer;
begin
  if FRuns=nil then Exit;             // kurucu, FRuns'tan önce durmuş olabilir
  for i:= 0 to FRuns.Count- 1 do
    TXLSXRichTextRun(FRuns[i]).Free;
  FRuns.Clear;
end;

destructor TXLSXCells.Destroy;
begin
  FPackedStorageEnabled:= False;
  if FList<>nil then Clear;          // TList.Create başarısız olduysa FList nil'dir
  // ... paketli depolama, dizge tabloları ve FList Free ile serbest bırakılır
  inherited Destroy;
end;

Başarısızlık noktası kesindir. Kurulum sırasındaki yığın ayırmalarını sayarsanız ilk ayırma örneğin kendisidir; NewInstance başarısız olursa hiçbir kurucu gövdesi çalışmaz ve hiçbir yıkıcı çağrılmaz, o durum her zaman temizdir. İkinci ayırma TList'tir. O başarısız olduğunda FRuns ya da FList nil kalır, RTL Destroy çağırır ve korumasız bir Destroy, bir nil başvurusu üzerinden Count okur; tam da bu, ikincil access violation'dır. TXLSXCells.Create içindeki sonraki başarısızlıklar zararsızdır, çünkü FList bir kez var olduğunda Clear, paketli depolamayı kullanmadan önce FPackedStorage <> nil kontrolü yapar

Delphi'de HotXLS kurucuları için ayırma sırası haritası: başarısız NewInstance temizdir, başarısız TList.Create alanı nil bırakır, sonraki başarısızlıklar korumalı kalır
yalnızca ikinci ayırma önemlidir: TList başarısızlığı alanı nil bırakır ve korumasız bir Destroy, EOutOfMemory'yi bir access violation'a çevirendir

Bu sınıflar önemli, çünkü toplu üretiliyorlar. Yüklenen her çalışma sayfası bir TXLSXCells koleksiyonu kurar ve biçimlendirilmiş parçaları olan her hücre ya da yorum bir TXLSXRichText kurar. Ağır zengin metinli büyük bir çalışma kitabı binlercesini üretir; bellek baskısı altında sonunda başarısız olan ayırmanın bu satırlardan birine düşme şansı gerçektir

Free nil-güvenlidir, alan erişimi değildir

TObject.Free, Destroy çağırmadan önce Self <> nil kontrolü yapar; nil bir alanda FRuns.Free işlem yapılmayan bir çağrıdır. FRuns.Count okumak öyle değildir: Count, örnek işaretçisinden bir ofsetteki alanı okur ve FRuns.Clear gibi sanal bir çağrı önce örnekten VMT işaretçisini okur. İkisi de nil üzerinde fault eder. Tuzak tam olarak bu asimetridir. Tipik bir yıkıcıdaki serbest bırakma çağrıları çoktan güvenlidir; önlerine konan döngüler ve temizlik çağrıları, belanın yattığı yerdir. Küçük bir kendi sınıfınızda desen şöyle:

type
  TStyledText = class
  private
    FRuns: TList;
    FIndex: TStringList;
  public
    constructor Create;
    destructor Destroy; override;
  end;

constructor TStyledText.Create;
begin
  inherited Create;
  FRuns := TList.Create;          // Buradaki EOutOfMemory FRuns'i nil bırakır
  FIndex := TStringList.Create;   // ... ve buradaki FIndex'i nil bırakır
end;

destructor TStyledText.Destroy;   // KÖTÜ: kurucunun bittiğini varsayar
var
  I: Integer;
begin
  for I := 0 to FRuns.Count - 1 do   // FRuns = nil olduğunda access violation
    TObject(FRuns[I]).Free;
  FIndex.Free;
  FRuns.Free;
  inherited Destroy;
end;

Düzeltilmiş yıkıcı, her başvuruyu korumaya alır ve alanları yalnızca nil-güvenli çağrılarla serbest bırakır:

destructor TStyledText.Destroy;
var
  I: Integer;
begin
  if FRuns <> nil then
    for I := 0 to FRuns.Count - 1 do
      TObject(FRuns[I]).Free;
  FreeAndNil(FIndex);
  FreeAndNil(FRuns);
  inherited Destroy;
end;

Buradan çıkan kurallar, kod incelemesinde uygulanacak kadar kısadır:

  • Bir yıkıcıda sahiplenilen bir alanın Free ya da FreeAndNil dışındaki her kullanımı, nil ya da Assigned kontrolü ister; alanı gezdiren Clear ya da ClearCore gibi yardımcılara yapılan çağrılar dahil
  • Sahiplenilen nesneleri Free ya da FreeAndNil ile serbest bırakın, asla doğrudan Destroy çağırarak değil; o, nil testini atlatır
  • inherited Destroy çağrısını son ifade olarak tutun; böylece ata, sizin alanlarınız gidip kendi alanlarını söker
  • Kurucudaki alan sırasından akıl yürütme; sonraki bakımcı, sizinkinin üstüne bir ayırma ekler ve başarısızlık noktasını sessizce kaydırır

inherited Create nereye ait?

inherited Create çağrısını en başa koyun; iki HotXLS kurucusunun ikisi de öyle yapıyor. Böylece atanın alanları, sizin kodunuz çalışmadan önce var olur ve yıkıcının ayna görüntüsü olan en sondaki inherited Destroy, ters sırada söker. Ata kurucusunun kendisi istisna fırlatırsa yıkıcınız yine çalışır ve sizin alanlarınızın her biri nildir; nil-güvenli bir yıkıcı bunu özel durum olmadan halleder. Kurucuda FOwner := nil gibi açık atamalar, InitInstance sonrasında gereksizdir ama zararsızdır ve bazen belge değeri taşır

Kurucu içinde yakalayıp serbest bırakmayın

İyi niyetli iki desen işi kötüleştirir. İlki, kurucu gövdesini try ... except Free; raise; end ile sarmaktır: RTL yıkıcıyı zaten çağıracak, dolayısıyla nesne iki kez yok edilir. İkincisi, "devam edelim" diye istisnayı yutmaktır: çağıran sonra, nil alanlı bir nesneye, geçerli görünen bir başvuru alır ve başarısızlık çok sonra ilgisiz bir metot çağrısına taşınır. Kurucu tutmadığı geçici bir nesne yaratıyorsa o geçiciyi yerel bir try ... finally içinde serbest bırakın ve istisnanın dokunulmadan yukarıya sürdürülmesine izin verin

Kurucu başarısızlık yollarını nasıl test edersiniz?

Bir kurucudaki her ayırma noktasını bilerek başarısız kılayım, birer birer, ve her seferinde EOutOfMemory isteyin; ardından normal ve başarılı bir kurulum gelsin. Bunun için gereken araç, GetMem, AllocMem ve ReallocMem işlevleri pozitif boyutlu istekleri sayan ve seçilen bir sırada nil döndüren ikame bir bellek yöneticisidir. Önce bir kurucunun hedef derlemenizde kaç ayırma yaptığını sayın, sonra sıra başına bir enjekte edilmiş başarısızlıkla koşun

Her enjekte edilmiş başarısızlıktan sonra aynı sınıfı, ayırıcı geri yüklenmişken yeniden kurun ve çalıştırın: bir zengin metin sınıfı için bir parça ekleyin, metnini kurun ve sade metnin beklendiği gibi geldiğini teyit edin. Normal yaşam döngüsü kontrolleri de yanında durmalı; örneğin biçimlendirilmiş parçalı ve yorumlu bir çalışma kitabını bir dosyaya ve bir akışa kaydetmek, kaynak çalışma kitabını serbest bırakmak ve iki kopyayı yeniden açıp karşılaştırmak

Ayırma sayıları tek bir derleyici ve platforma aittir; öteki bir derleyici ya da RTL farklı ayırabilir, o yüzden dağıttığınız derlemede ölçün. HotXLS aynı nil-güvenli kuralı kütüphanenin geneline uygular; stil, grafik ve pivot koleksiyonlarından karma tablolara, zip yazıcısına ve PDF dışa aktarıcısına dek. Karma tablolar özel bir nota hak ediyor: yeniden ayırmadan önce maskesini değiştiren ve yeniden ayırma sonra başarısız olan bir tablo, yıkıcısını eski tablonun ötesindeki bellekte gezdirir; maske, yalnızca yeni depolama var olduktan sonra değişmelidir. Bir durum RTL'nin kendi içinde duruyor: TInterfaceList, iç ayırması başarısız olduğunda yıkıcısında fault ediyor. Bütün bunlar, yarısında başarısız olan keyfi bir işlemin geri alınması anlamına gelmez ve genel bir bellek yetersizliği kurtarma güvencesi değildir

Bu, büyük çalışma kitabı açan kod için ne değiştiriyor?

Pratikte bu kurulum yollarından gelen EOutOfMemory, bir çökme gibi tedavi etmek zorunda olduğunuz bir access violation yerine yakalayıp üzerine hareket edebileceğiniz bir sinyaldir. TXLSXWorkbook.Create parametre almaz ve TXLSXWorkbook.Open(const FileName: WideString): Integer başarıda 1, başarısızlıkta -1 döndürür. Yükleme sırasında fırlatılan istisnalar, çalışma kitabının Diagnostics koleksiyonuna xlsDiagnosticUnhandledException olarak kaydedilir ve sonra yeniden fırlatılır; davranış HotXLS yapılandırılmış teşhisler yazısında anlatılıyor. Yeniden deneme sarmalayıcısı şöyle görünür:

type
  EWorkbookOpenFailed = class(Exception);

function OpenWorkbookWithRetry(const FileName: WideString;
  MaxAttempts: Integer): TXLSXWorkbook;
var
  Attempt: Integer;
  Book: TXLSXWorkbook;
begin
  Result := nil;
  for Attempt := 1 to MaxAttempts do
  begin
    Book := nil;
    try
      try
        Book := TXLSXWorkbook.Create;          // kurucu argümanı yok
        if Book.Open(FileName) <> 1 then        // 1 = başarı, -1 = başarısızlık
          raise EWorkbookOpenFailed.CreateFmt('HotXLS could not open %s',
            [string(FileName)]);
        Result := Book;
        Book := nil;                           // sahiplik çağırana geçer
        Exit;
      finally
        Book.Free;                             // devirden sonra nil-güvenli
      end;
    except
      on E: EOutOfMemory do
      begin
        if Attempt = MaxAttempts then
          raise;
        ReleaseApplicationCaches;              // sizin kodunuz: önbellekleri, öteki kitapları bırakın
      end;
    end;
  end;
end;

O sarmalayıcıdaki birkaç ayrıntı bilinçli. Her deneme taze bir TXLSXWorkbook kurar, çünkü nil-güvenli yıkıcılar kurulum başarısızlıklarını temiz kılar ama yarım kalmış bir Open'ı sürdürülebilir kılmaz; başarısız örnek basitçe serbest bırakılır. Yalnızca EOutOfMemory yeniden denenir; EWorkbookOpenFailed, parola hataları ve geri kalan her şey ilk denemede yukarı taşınır. Bir EAccessViolation hiçbir zaman yeniden denememelidir; o kaynak kıtlığını değil bir kusuru işaret eder. Ve yeniden deneme, yalnızca denemeler arasında bir şey değişiyorsa mantıklıdır; işleyici bu yüzden önce bellek boşaltıyor. Başarısız denemenin teşhis izine ihtiyacınız varsa Book.Diagnostics değerini, finally bloğu onu serbest bırakmadan önce okuyun

Delphi'de HotXLS ile yeniden deneme karar matrisi: EOutOfMemory taze bir TXLSXWorkbook ile yeniden denenir, öteki başarısızlıklar anında yukarı taşınır, bir access violation hiç denenmez
her deneme taze bir workbook kurar, çünkü yarım kalmış bir Open sürdürülebilir değildir; yalnızca EOutOfMemory ikinci bir denemeyi hak eder

Yeniden deneme bir hafletme tedbiridir, kapasite planı değil. Büyük dosyalarda sürekli EOutOfMemory yiyen bir 32 bit süreç, genelde fiziksel bellekten çok bitişik adres alanı kıtlığı çekiyordur ve kalıcı cevap bir Win64 derlemesidir. Aynı akıl yazma tarafında da geçerli; HotXLS aşamalı geçici dosya kaydetmeleriyle çökme güvenli kaydetmeleri, başarısız bir kaydetmenin mevcut dosyaya zarar vermesini engelliyor. Birden çok iş parçacığı tek bir çalışma kitabını paylaşıyorsa HotXLS okuma kiraları ve yazma koruyucuları, o örneğe erişimin nasıl koordine edildiğini anlatıyor

Kendi Delphi sınıflarınız için bir kontrol listesi

Korumaların kendisi her biri tek satırdır ve aynı denetim, kendi kodunuzda dakikalar alır. Alanları serbest bırakmadan önce bir Clear, Reset ya da ClearCore metodu çağıran yıkıcıları arayın, sonra o metodun, kurucunun ulaşmamış olabileceği bir alana dokunup dokunmadığına bakın. Önünde nil kontrolü olmayan, sahiplenilen listeler üzerinde döngü arayın. İstisna yakalayan kurucuları arayın; hem yutmayı hem manuel Freeyi kaldırın. İnceleme yerine kanıt istiyorsanız yukarıda anlatılan sayan bellek yöneticisi, bir sayfalık koda sığar ve bir kurucudaki her ayırmayı bir test durumuna çevirir

Tam özellik seti, desteklenen Delphi ve C++Builder sürümleri ve lisans için bkz. HotXLS Delphi Excel bileşeni ürün sayfası