Verzögerte Initialisierung

In der Computer-Programmierung ist eine verzögerte Initialisierung ein Entwurfsmuster, das die Erstellung eines Objekts, die Berechnung eines Werts oder eines anderen teuren Prozesses erst dann erstellt, wenn es erstmals benötigt wird. Es ist eine Art verzögerte Auswertung, die sich speziell auf die Instanziierung von Objekten oder anderen Ressourcen bezieht.

Eine verzögerte Initialisierung wird erreicht, indem eine Zugriffs-Methode (eine Get-Methode einer Eigenschaft, auch Getter genannt) erweitert wird, um zu überprüfen, ob ein privates Mitglied (Member), das als Cache fungiert, bereits initialisiert wurde. Wenn dies der Fall ist, wird es sofort zurückgegeben. Wenn nicht, wird eine neue Instanz erstellt, in die Member-Variable eingefügt und just-in-time (JIT) zur ersten Verwendung an den Aufrufer zurückgegeben.

Wenn Objekte Eigenschaften haben, die selten verwendet werden, kann dies die Startgeschwindigkeit verbessern. Die durchschnittliche Programmausführung kann in Bezug auf Speicher (für die Bedingungs-Variablen) und Ausführungs-Zyklen (um sie zu überprüfen) etwas schlechter sein. Die Auswirkung auf die Objektinstanziierung wird zeitlich verteilt („amortisiert“) und verzögern nicht in die Startphase des Systems. Damit können die mittleren Antwortzeiten erheblich verbessert werden.

In Multithreading-Code muss der Zugriff auf verzögert-initialisierte Objekte und Status synchronisiert werden, um eine Wettlaufsituation (race condition) zu vermeiden.

Die träge/verzögerte Fabrik („lazy factory“)

[Bearbeiten | Quelltext bearbeiten]

In einer Software-Entwurfsmuster-Ansicht (eng. „OOP View“) wird die verzögerte Initialisierung häufig zusammen mit einer Fabrikmethode (OOP Muster) verwendet. Dies kombiniert drei Ideen:

  • Verwendung einer Fabrik-Methode zum Erstellen von Instanzen einer Klasse (factory method pattern)
  • Speichern der Instanzen in einer Zuordnungstabelle und Zurückgeben derselben Instanz an jede Anforderung für eine Instanz mit denselben Parametern (Multiton-Muster)
  • Verwenden der verzögerten Initialisierung zum Instanziieren des Objekts bei der ersten Anforderung (verzögertes Initialisierungsmuster)

Folgendes Beispiel zeigt eine Klasse mit verzögerter Initialisierung in ActionScript:

package examples.lazyinstantiation
{
	public class Fruit
	{
		private var _typeName:String;
		private static var instancesByTypeName:Dictionary = new Dictionary();

		public function Fruit(typeName:String):void
		{
			this._typeName = typeName;
		}

		public function get typeName():String
		{
			return _typeName;
		}

		public static function getFruitByTypeName(typeName:String):Fruit
		{
			return instancesByTypeName[typeName] ||= new Fruit(typeName);
		}

		public static function printCurrentTypes():void
		{
			for each (var fruit:Fruit in instancesByTypeName)
			{
				// Iteriert über alle Werte
				trace(fruit.typeName);
			}
		}
	}
}

Anwendung:

package
{
	import examples.lazyinstantiation;

	public class Main
	{
		public function Main():void
		{
			Fruit.getFruitByTypeName("Banane");
			Fruit.printCurrentTypes();

			Fruit.getFruitByTypeName("Apfel");
			Fruit.printCurrentTypes();

			Fruit.getFruitByTypeName("Banane");
			Fruit.printCurrentTypes();
		}
	}
}

In C wird eine verzögerte Auswertung normalerweise in einer einzelnen Funktion oder einer einzelnen Quelldatei unter Verwendung statischer Variablen implementiert.

In einer Funktion:

#include <string.h>
#include <stdlib.h>
#include <stddef.h>
#include <stdio.h>

struct fruit {
    char *name;
    struct fruit *next;
    int number;
    /* Andere Eigenschaften */
};

struct fruit *get_fruit(char *name) {
    static struct fruit *fruit_list;
    static int seq;
    struct fruit *f;
    for (f = fruit_list; f; f = f->next)
        if (0 == strcmp(name, f->name))
            return f;
    if (!(f = malloc(sizeof(struct fruit))))
        return NULL;
    if (!(f->name = strdup(name))) {
        free(f);
        return NULL;
    }
    f->number = ++seq;
    f->next = fruit_list;
    fruit_list = f;
    return f;
}

/* Beispiel Code */

int main(int argc, char *argv[]) {
    int i;
    struct fruit *f;
    if (argc < 2) {
        fprintf(stderr, "Aufruf: fruits Frucht-Name [...]\n");
        exit(1);
    }
    for (i = 1; i < argc; i++) {
        if ((f = get_fruit(argv[i]))) {
            printf("Frucht %s: Anzahl %d\n", argv[i], f->number);
        }
    }
    return 0;
}

Wenn man stattdessen eine einzelne Quelldatei verwendet, kann der Status von mehreren Funktionen gemeinsam genutzt werden, während er dennoch vor nicht zugehörigen/verwandten Funktionen verborgen bleibt.

fruit.h (Header-Datei):

#ifndef _FRUIT_INCLUDED_
#define _FRUIT_INCLUDED_

struct fruit {
    char *name;
    struct fruit *next;
    int number;
    /* Andere Eigenschaften */
};

struct fruit *get_fruit(char *name);
void print_fruit_list(FILE *file);

#endif /* _FRUIT_INCLUDED_ */

fruit.c:

#include <string.h>
#include <stdlib.h>
#include <stddef.h>
#include <stdio.h>
#include "fruit.h"

static struct fruit *fruit_list;
static int seq;

struct fruit *get_fruit(char *name) {
    struct fruit *f;
    for (f = fruit_list; f; f = f->next)
        if (0 == strcmp(name, f->name))
            return f;
    if (!(f = malloc(sizeof(struct fruit))))
        return NULL;
    if (!(f->name = strdup(name))) {
        free(f);
        return NULL;
    }
    f->number = ++seq;
    f->next = fruit_list;
    fruit_list = f;
    return f;
}

void print_fruit_list(FILE *file) {
    struct fruit *f;
    for (f = fruit_list; f; f = f->next)
        fprintf(file, "%4d  %s\n", f->number, f->name);
}

main.c:

#include <stdlib.h>
#include <stdio.h>
#include "fruit.h"

int main(int argc, char *argv[]) {
    int i;
    struct fruit *f;
    if (argc < 2) {
        fprintf(stderr, "Aufruf: Frucht-Name [...]\n");
        exit(1);
    }
    for (i = 1; i < argc; i++) {
        if ((f = get_fruit(argv[i]))) {
            printf("Frucht %s: Anzahl %d\n", argv[i], f->number);
        }
    }
    printf("Folgende Fruechte wurden generiert:\n");
    print_fruit_list(stdout);
    return 0;
}

In .Net-Framework 4.0 hat Microsoft eine Lazy Klasse inkludiert, mit der das Verzögertes-Laden durchgeführt werden kann. Unten finden Sie einen Dummy-Code, mit der Klasse Fruit verzögert geladen werden kann.

var lazyFruit = new Lazy<Fruit>();
Fruit fruit = lazyFruit.Value;

Hier ist ein Beispiel in C#.

Die Fruit Klasse selbst tut an dieser Stelle Nichts. Die Klassen-Variable _typesDictionary ist ein Dictionary/Map mit dem Fruit Instanzen nach typeName gespeichert werden.

using System;
using System.Collections;
using System.Collections.Generic;

public class Fruit
{
    private string _typeName;
    private static IDictionary<string, Fruit> _typesDictionary = new Dictionary<string, Fruit>();

    private Fruit(String typeName)
    {
        this._typeName = typeName;
    }

    public static Fruit GetFruitByTypeName(string type)
    {
        Fruit fruit;

        if (!_typesDictionary.TryGetValue(type, out fruit))
        {
            // Verzögerte Initialisierung
            fruit = new Fruit(type);

            _typesDictionary.Add(type, fruit);
        }
        return fruit;
    }

    public static void ShowAll()
    {
        if (_typesDictionary.Count > 0)
        {
            Console.WriteLine("Anzahl erzeugte Instanzen = {0}", _typesDictionary.Count);

            foreach (KeyValuePair<string, Fruit> kvp in _typesDictionary)
            {
                Console.WriteLine(kvp.Key);
            }

            Console.WriteLine();
        }
    }

    public Fruit()
    {
        // Wird benötigt, damit dieses Beispiel kompiliert werden kann.
    }
}

class Program
{
    static void Main(string[] args)
    {
        Fruit.GetFruitByTypeName("Banane");
        Fruit.ShowAll();

        Fruit.GetFruitByTypeName("Apfel");
        Fruit.ShowAll();

        // Liefert den ersten erzeugten Instanz ("Banane") zurück.
        Fruit.GetFruitByTypeName("Banane");
        Fruit.ShowAll();

        Console.ReadLine();
    }
}

Ein einfaches Beispiel für die verzögerte Initialisierung, mit der Ausnahme, dass hierfür eine Aufzählung für den Typ verwendet wird

namespace DesignPatterns.LazyInitialization
{
    public class LazyFactoryObject
    {
        //internal collection of items
        //IDictionaery makes sure they are unique
        private IDictionary<LazyObjectSize, LazyObject> _LazyObjectList =
            new Dictionary<LazyObjectSize, LazyObject>();

        //enum for passing name of size required
        //avoids passing strings and is part of LazyObject ahead
        public enum LazyObjectSize
        {
            None,
            Small,
            Big,
            Bigger,
            Huge
        }

        //standard type of object that will be constructed
        public struct LazyObject
        {
            public LazyObjectSize Size;
            public IList<int> Result;
        }

        //takes size and create 'expensive' list
        private IList<int> Result(LazyObjectSize size)
        {
            IList<int> result = null;

            switch (size)
            {
                case LazyObjectSize.Small:
                    result = CreateSomeExpensiveList(1, 100);
                    break;
                case LazyObjectSize.Big:
                    result = CreateSomeExpensiveList(1, 1000);
                    break;
                case LazyObjectSize.Bigger:
                    result = CreateSomeExpensiveList(1, 10000);
                    break;
                case LazyObjectSize.Huge:
                    result = CreateSomeExpensiveList(1, 100000);
                    break;
                case LazyObjectSize.None:
                    result = null;
                    break;
                default:
                    result = null;
                    break;
            }

            return result;
        }

        //not an expensive item to create, but you get the point
        //delays creation of some expensive object until needed
        private IList<int> CreateSomeExpensiveList(int start, int end)
        {
            IList<int> result = new List<int>();

            for (int counter = 0; counter < (end - start); counter++)
            {
                result.Add(start + counter);
            }

            return result;
        }

        public LazyFactoryObject()
        {
            //empty constructor
        }

        public LazyObject GetLazyFactoryObject(LazyObjectSize size)
        {
            //yes, i know it is illiterate and inaccurate
            LazyObject noGoodSomeOne;

            //retrieves LazyObjectSize from list via out, else creates one and adds it to list
            if (!_LazyObjectList.TryGetValue(size, out noGoodSomeOne))
            {
                noGoodSomeOne = new LazyObject();
                noGoodSomeOne.Size= size;
                noGoodSomeOne.Result = this.Result(size);

                _LazyObjectList.Add(size, noGoodSomeOne);
            }

            return noGoodSomeOne;
        }
    }
}

Hier ein Beispiel in C++.

#include <iostream>
#include <map>
#include <string>

class Fruit {
 public:
  static Fruit* GetFruit(const std::string& type);
  static void PrintCurrentTypes();

 private:
  // Note: constructor private forcing one to use static |GetFruit|.
  Fruit(const std::string& type) : type_(type) {}

  static std::map<std::string, Fruit*> types;

  std::string type_;
};

// static
std::map<std::string, Fruit*> Fruit::types;

// Lazy Factory method, gets the |Fruit| instance associated with a certain
// |type|.  Creates new ones as needed.
Fruit* Fruit::GetFruit(const std::string& type) {
  auto [it, inserted] = types.emplace(type, nullptr);
  if (inserted) {
    it->second = new Fruit(type);
  }
  return it->second;
}

// For example purposes to see pattern in action.
void Fruit::PrintCurrentTypes() {
  std::cout << "Number of instances made = " << types.size() << std::endl;
  for (const auto& [type, fruit] : types) {
    std::cout << type << std::endl;
  }
  std::cout << std::endl;
}

int main() {
  Fruit::GetFruit("Banana");
  Fruit::PrintCurrentTypes();

  Fruit::GetFruit("Apple");
  Fruit::PrintCurrentTypes();

  // Returns pre-existing instance from first time |Fruit| with "Banana" was
  // created.
  Fruit::GetFruit("Banana");
  Fruit::PrintCurrentTypes();
}

// OUTPUT:
//
// Number of instances made = 1
// Banana
//
// Number of instances made = 2
// Apple
// Banana
//
// Number of instances made = 2
// Apple
// Banana
//
class Fruit
  private getter type : String
  @@types = {} of String => Fruit

  def initialize(@type)
  end

  def self.get_fruit_by_type(type : String)
    @@types[type] ||= Fruit.new(type)
  end

  def self.show_all
    puts "Number of instances made: #{@@types.size}"
    @@types.each do |type, fruit|
      puts "#{type}"
    end
    puts
  end

  def self.size
    @@types.size
  end
end

Fruit.get_fruit_by_type("Banana")
Fruit.show_all

Fruit.get_fruit_by_type("Apple")
Fruit.show_all

Fruit.get_fruit_by_type("Banana")
Fruit.show_all

Ausgabe:

Number of instances made: 1
Banana

Number of instances made: 2
Banana
Apple

Number of instances made: 2
Banana
Apple

Beispiel in Haxe[1]

class Fruit {
  private static var _instances = new Map<String, Fruit>();

  public var name(default, null):String;

  public function new(name:String) {
    this.name = name;
  }

  public static function getFruitByName(name:String):Fruit {
    if (!_instances.exists(name)) {
      _instances.set(name, new Fruit(name));
    }
    return _instances.get(name);
  }

  public static function printAllTypes() {
    trace([for(key in _instances.keys()) key]);
  }
}

Anwendung

class Test {
  public static function main () {
    var banana = Fruit.getFruitByName("Banana");
    var apple = Fruit.getFruitByName("Apple");
    var banana2 = Fruit.getFruitByName("Banana");

    trace(banana == banana2); // true. same banana

    Fruit.printAllTypes(); // ["Banana","Apple"]
  }
}

Beispiel in Java.

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

public class Program {

    /**
     * @param args
     */
    public static void main(String[] args) {
        Fruit.getFruitByTypeName(FruitType.banana);
        Fruit.showAll();
        Fruit.getFruitByTypeName(FruitType.apple);
        Fruit.showAll();
        Fruit.getFruitByTypeName(FruitType.banana);
        Fruit.showAll();
    }
}

enum FruitType {
    none,
    apple,
    banana,
}

class Fruit {

    private static Map<FruitType, Fruit> types = new HashMap<>();

    /**
     * Using a private constructor to force the use of the factory method.
     * @param type
     */
    private Fruit(FruitType type) {
    }

    /**
     * Lazy Factory method, gets the Fruit instance associated with a certain
     * type. Instantiates new ones as needed.
     * @param type Any allowed fruit type, e.g. APPLE
     * @return The Fruit instance associated with that type.
     */
    public static Fruit getFruitByTypeName(FruitType type) {
        Fruit fruit;
                // This has concurrency issues.  Here the read to types is not synchronized,
                // so types.put and types.containsKey might be called at the same time.
                // Don't be surprised if the data is corrupted.
        if (!types.containsKey(type)) {
            // Lazy initialisation
            fruit = new Fruit(type);
            types.put(type, fruit);
        } else {
            // OK, it's available currently
            fruit = types.get(type);
        }

        return fruit;
    }

    /**
     * Lazy Factory method, gets the Fruit instance associated with a certain
     * type. Instantiates new ones as needed. Uses double-checked locking
     * pattern for using in highly concurrent environments.
     * @param type Any allowed fruit type, e.g. APPLE
     * @return The Fruit instance associated with that type.
     */
    public static Fruit getFruitByTypeNameHighConcurrentVersion(FruitType type) {
        if (!types.containsKey(type)) {
            synchronized (types) {
                // Check again, after having acquired the lock to make sure
                // the instance was not created meanwhile by another thread
                if (!types.containsKey(type)) {
                    // Lazy initialisation
                    types.put(type, new Fruit(type));
                }
            }
        }

        return types.get(type);
    }

    /**
     * Displays all entered fruits.
     */
    public static void showAll() {
        if (types.size() > 0) {

           System.out.println("Number of instances made = " + types.size());

            for (Entry<FruitType, Fruit> entry : types.entrySet()) {
                String fruit = entry.getKey().toString();
                fruit = Character.toUpperCase(fruit.charAt(0)) + fruit.substring(1);
                System.out.println(fruit);
            }

            System.out.println();
        }
    }
}

Ausgabe

Number of instances made = 1
Banana

Number of instances made = 2
Banana
Apple

Number of instances made = 2
Banana
Apple

Hier ist ein Beispiel in JavaScript.

var Fruit = (function() {
  var types = {};
  function Fruit() {};

  // count own properties in object
  function count(obj) {
    return Object.keys(obj).length;
  }

  var _static = {
    getFruit: function(type) {
      if (typeof types[type] == 'undefined') {
        types[type] = new Fruit;
      }
      return types[type];
    },
    printCurrentTypes: function () {
      console.log('Number of instances made: ' + count(types));
      for (var type in types) {
        console.log(type);
      }
    }
  };

  return _static;

})();

Fruit.getFruit('Apple');
Fruit.printCurrentTypes();
Fruit.getFruit('Banana');
Fruit.printCurrentTypes();
Fruit.getFruit('Apple');
Fruit.printCurrentTypes();

Ausgabe

Number of instances made: 1
Apple

Number of instances made: 2
Apple
Banana

Number of instances made: 2
Apple
Banana

Hier ist ein Beispiel für die verzögerte Initialisierung in PHP 7.4:

<?php
header('Content-Type: text/plain; charset=utf-8');

class Fruit
{
    private string $type;
    private static array $types = array();

    private function __construct(string $type)
    {
        $this->type = $type;
    }

    public static function getFruit(string $type)
    {
        // Lazy initialization takes place here
        if (!isset(self::types[$type])) {
            self::types[$type] = new Fruit($type);
        }

        return self::types[$type];
    }

    public static function printCurrentTypes(): void
    {
        echo 'Number of instances made: ' . count(self::types) . "\n";
        foreach (array_keys(self::types) as $key) {
            echo "$key\n";
        }
        echo "\n";
    }
}

Fruit::getFruit('Apple');
Fruit::printCurrentTypes();

Fruit::getFruit('Banana');
Fruit::printCurrentTypes();

Fruit::getFruit('Apple');
Fruit::printCurrentTypes();

/*
OUTPUT:

Number of instances made: 1
Apple

Number of instances made: 2
Apple
Banana

Number of instances made: 2
Apple
Banana
* /

Hier ist ein Beispiel in Python.

class Fruit:
    def __init__(self, item: str) -> None:
        self.item = item

class Fruits:
    def __init__(self) -> None:
        self.items = {}

    def get_fruit(self, item: str) -> Fruit:
        if item not in self.items:
            self.items[item] = Fruit(item)

        return self.items[item]

if __name__ == "__main__":
    fruits = Fruits()
    print(fruits.get_fruit("Apple"))
    print(fruits.get_fruit("Lime"))

Hier ist ein Beispiel in Ruby, wie ein Authentifizierungs-Token von einem Remote-Dienst wie Google verzögert initialisiert werid. Die Art wie @auth_token zwischengespeichert wird, ist auch ein Beispiel für die Memoisierung.

require 'net/http'
class Blogger
  def auth_token
    @auth_token ||=
      (res = Net::HTTP.post_form(uri, params)) &&
      get_token_from_http_response(res)
  end

  # get_token_from_http_response, uri and params are defined later in the class
end

b = Blogger.new
b.instance_variable_get(:@auth_token) # returns nil
b.auth_token # returns token
b.instance_variable_get(:@auth_token) # returns token

Scala verfügt über eine integrierte Unterstützung für die verzögerte Initiierung von Variablen.[2]

 scala> val x = { println("Hello"); 99 }
 Hello
 x: Int = 99
 scala> lazy val y = { println("Hello!!"); 31 }
 y: Int = <lazy>
 scala> y
 Hello!!
 res2: Int = 31
 scala> y
 res3: Int = 31

Hier ist ein Beispiel für eine typische Zugriffs-Methode in Smalltalk, um den Wert einer Variable durch Nutzung von verzögertes Initialisieren zurückzuliefern.

    height
        ^height ifNil: [height := 2.0].

Die 'nicht-verzögerte' Alternative besteht darin, eine Initialisierungs-Methode zu verwenden, die beim Erstellen des Objekts ausgeführt wird, und dann eine einfachere Zugriffsmethode zum Abrufen des Werts zu verwenden.

    initialize
        height := 2.0

    height
        ^height

Beachten Sie, dass die verzögerte Initialisierung auch in nicht-objektorientierte Sprachen verwendet werden kann.

Theoretische Informatik

[Bearbeiten | Quelltext bearbeiten]

Auf dem Gebiet der theoretischen Informatik ist die verzögerte Initialisierung (auch als verzögertes Array bezeichnet) eine Technik zum Entwerfen von Datenstrukturen, die mit Speicher arbeiten können, welcher nicht zuvor initialisiert werden muss. Nehmen wir an, wir haben Zugriff auf eine Tabelle T von n nicht initialisierten Speicherzellen (nummeriert von 1 bis n) und möchten m Zellen dieses Arrays zuweisen, z. B. möchten wir T [ki]: = vi für Paare zuweisen ( k1, v1), ..., (km, vm), wobei alle ki unterschiedlich sind. Die Technik der verzögerten Initialisierung ermöglicht es uns, dies in nur O (m) -Operationen zu tun, anstatt O (m + n) -Operationen auszugeben, um zuerst alle Array-Zellen zu initialisieren. Die Technik besteht einfach darin, eine Tabelle V zuzuweisen, in der die Paare (ki, vi) in einer beliebigen Reihenfolge gespeichert sind, und für jedes i in die Zelle T [ki] die Position in V zu schreiben, an der der Schlüssel ki gespeichert ist, wobei die übrigen Zellen von T nicht initialisiert werden. Dies kann verwendet werden, um Abfragen auf folgende Weise zu behandeln: Wenn wir die Zelle T [k] für einige k nachschlagen, können wir prüfen, ob k im Bereich {1, ..., m} liegt: Wenn dies nicht der Fall ist, dann ist T [k] nicht initialisiert. Andernfalls überprüfen wir V [T [k]] und stellen sicher, dass die erste Komponente dieses Paares gleich k ist. Ist dies nicht der Fall, ist T [k] nicht initialisiert (und ist zufällig in den Bereich {1, ..., m} gefallen). Ansonsten wissen wir, dass T [k] tatsächlich eine der initialisierten Zellen ist und der entsprechende Wert die zweite Komponente des Paares ist.

Einzelnachweise

[Bearbeiten | Quelltext bearbeiten]
  1. Lazy initialization - Design patterns - Haxe programming language cookbook, 11. Januar 2018. Abgerufen am 9. November 2018 (englisch). 
  2. David Pollak: Beginning Scala : Description based on print version record. - Includes index. Learn the powerful Scala functional-object language in a fun, interactive way (= Expert's voice in open source). Apress, 2009, ISBN 978-1-4302-1989-7 (englisch).