Zum Inhalt springen

C# Vererbung und Instanzierung


redstav

Empfohlene Beiträge

Moin ihr Guten,

ich dachte ich hätte die Initialisierung von Objekten verstanden bis ich vor kurzem einen abgefahrenen fortgeschritteneren Code gelesen habe.(War bestimmt nicht so krass aber hat Dinge angestellt die mir neue waren.

Hab mich jetzt damit auseinandergesetzt was bei "Objekttyp objekt = new Constructor" genau abgeht. Mir war zum beispiel nicht explizit klar dass ersteres der Objekttyp und letzteres der Constructor ist.

Jetzt hab ich anhand eines Beispiels eines Buches kurz rumprobiert, so lernt man ja bekanntlich am besten und am nachhaltigsten.. ich zumindest.. und bin auf den Fall gestoßen:

Eine Abstrakte SuperKlasse (ASK) (Studenten), Zwei SubKlassen(SK) (FtStudent, PtStudent) und eine Klasse(E) die Objekte(O) der ASK entgegennimmt.

Den Code dazu gibt es gleich unten.

ASK O = new ASK geht offensichtlicherweiße nicht. ASK ist abstrakt.

Die Kombination SK O = new SK kann im Prinzip alles machen und wird von E auch als Objekt von ASK erkannt.

ASK O = new SK... O lässt sich erzeugen mit dem SK Constructor. Das Objekt ist aber vom Typ ASK... Sollte doch eigentlich nicht gehen weil ASK Abstrakt ist und eigentlich mehr eine Idee als ein reales Objekt wäre. O kann in diesem Fall auch keine Methoden von SK verwenden. Ist ja die übergeordnete Klasse. Macht irgendwie Sinn denke ich. Warum sollte ich es jedoch mit SK konstruieren wenn ich die Methoden etc ohnehin nicht nutzen kann.

 

Und wenn der Compiler direkt versteht dass die SubKlassen zur Abstrakten SuperKlasse gehören und sie entgegennimmt, warum versteht das Objekt nicht

Welche Anwendungsfälle gäbe es den in denen das nützlich wäre?

(Die Zahlen sind pure Willkür. Die namen im Prinzip auch. Die Logik wie der Compiler mit dem ganzen umgeht ist für mich das hier interessante.)

Grüße und schon mal vielen Dank für euren Input und eure Zeit!

namespace Student
{
    abstract class Studenten
    {
        int loan;
        public int Loan
        {
            get { return loan; }
            set { loan = value; }
        }
        public int getLoan()
        {
            return loan;
        }
        public void PrintTimetable()
        {
            Console.WriteLine("Timetable");
        }
    }


    class PtStudent : Studenten
    {
        public void ApplyForJob()
        {
            Loan -= 200;
        }
    }

    class FtStudent : Studenten
    {
        public void ApplyForLoan()
        {
            Loan += 200;
        }
    }

    class Lecturer
    {
        public void Help(Studenten s)
        {
            s.Loan += 500;
        }
    }


}
namespace Student
{
    class Program
    {
        static void Main(string[] args)
        {

            Studenten s = new FtStudent();
            Lecturer L = new Lecturer();
            FtStudent d = new FtStudent();
            Studenten e = new PtStudent();
            PtStudent f = new PtStudent();

            Console.WriteLine("S S-FT");
            s.PrintTimetable();
            L.Help(s); //+200
            Console.WriteLine(s.getLoan());

            Console.WriteLine("D FT-FT");
            d.PrintTimetable();
            d.ApplyForLoan(); //+200
            Console.WriteLine(d.getLoan());
            L.Help(d); //+500
            Console.WriteLine(d.getLoan());

            Console.WriteLine("E S-PT");
            e.PrintTimetable();
            L.Help(e); //+500
            Console.WriteLine(e.getLoan());

            Console.WriteLine("F PT-PT");
            f.PrintTimetable();
            f.ApplyForJob(); //-200
            Console.WriteLine(f.getLoan());
            L.Help(f); //+500
            Console.WriteLine(f.getLoan());
            Console.ReadKey();
            
            /* Geht nicht.
             * Studenten x = new Studenten(); Abstrakt, macht Sinn.
             * s.ApplyForLoan();
             * e.ApplyForJob();
             */
        }
    }
}

 

Link zu diesem Kommentar
Auf anderen Seiten teilen

Worum es hier geht ist das sogenannte Boxing und Unboxing.

Beim Boxing wird die Variable eines Types (FtStudent) in einen anderen Typ (von dem er ableitet, hier: Student) (implizit) konvertiert und fortan kannst du mit diesem nur noch so arbeiten wie mit dem Basistyp (Student). Die Variable ist aber trotzdem noch vom ursprünglichen Typ (FtStudent).

FtStudent ft = new FtStudent();
Student boxed = ft;

// boxed hat nur die Methoden von Student.

Willst du nun wieder die Methoden von FtStudent benutzen musst du die Variable unboxen. Dies geschieht explizit mit einem Cast.

FtStudent unboxed = (FtStudent) boxed;

// unboxed enthält die Methoden des FtStudent.


Wenn eine Variable also geboxt wird, wird sie fortan als Basistyp behandelt und hat nur dessen Methoden (und Properties etc.), auch wenn sie in Wirklichkeit immer noch von ursprünglichen Typ ist. Der Compiler lässt allerdings nicht zu, dass du die Methoden des unterliegenden Typen aufrufst, dazu musst du die Variable wieder unboxen.

(Hoffe das macht Sinn - wenn nicht such mal nach 'Boxing and Unboxing').

Wozu man das braucht? Wenn du eine abstrakte Klasse (oder meist ein Interface) definierst, dass die Methodensignaturen hat, welche eine Klasse implementieren musst, kannst du die Methoden, wo die Klasse hergenommen wird, so schreiben, dass sie nur die abstrakte Klasse/Interface erwartet.
Dies bedeutet, dass, wenn du eine andere Implementierung haben willst, du nur noch an wenigen Stellen die Klasse ändern musst, und die Parameter der Methode gar nicht anfassen musst.

// Methode braucht Student
public void DoSomething(Student s)
{
	// Do something.
}

// Im Moment ist deine Implementierung des Studenten FtStudent.
Student s = new FtStudent();
DoSomething(s);

//Wenn du jetzt die Implementierung ändern willst musst du nur ein Wort austauschen. So schaut dann der neue Code aus.
Student s = new PtStudent();
DoSomething(s);

 

Link zu diesem Kommentar
Auf anderen Seiten teilen

vor 3 Stunden schrieb 0x00:

(Hoffe das macht Sinn - wenn nicht such mal nach 'Boxing and Unboxing').

Das Thema hat absolut nichts mit Boxing und Unboxing zu tun, denn Boxing und Unboxing ist was anderes. Boxing und Unboxing wird verwendet, um Wertetypen (integer, double, ...) in Objekte zu wandeln (boxing) und von Objekten wieder zurück in Wertetypen (unboxing). Beispiel für Boxing:

int i = 123;
object o = i; // boxing

Beispiel für Unboxing:

o = 123;
i = (int)o;  // unboxing

So etwas wird verwendet, wenn man z.B. eine Liste mit unterschiedlichen Datentypen hat:

var list = new List<object>();

In diese Liste kann dann alles reingeschrieben werden. Sei es z.B. Strings oder Integers.

Das Thema, was @redstav anspricht, ist "Vererbung" und Vererbung ist die Haupteigenschaft der Objektorientierung.

Abstrakte Klassen können nicht instanziert werden. Sie können nur vererbt werden, wie in dem Beispiel. Mit abstrakten Klassen können nämlich Grundgerüste geschaffen werden, die dann mit den Subklassen verfeinert werden können. Das Grundgerüst garantiert uns, dass die Subklassen die Eigenschaften und Methoden der abstrakten Klasse besitzt.

Dein Beispiel finde ich aber nicht so toll, da es die abstrakten Klassen schlecht veranschaulicht. Ich versuche es mal mit einem anderen Beispiel: Angenommen wir wollen geometrische Figuren auf dem Bildschirm malen. Wir haben ein Quadrat und ein Kreis und wollen sie als Klassen abbilden. Beide Figuren sollen später mit einer Farbe ausgefüllt werden und sollen eine Methode besitzen, die dafür sorgt, dass die Figur auf dem Bildschirm gemalt wird. Fangen wir naiv an:

public class Square
{
    public double Width { get; }
    public Color Color { get; }

    public Square(double width, Color color)
    {
        this.Width = width;
        this.Color = color;
    }
  
    public void Paint()
    {
        // Malt Quadrat auf den Bildschirm
    }
}

public class Circle
{
    public double Radius { get; }
    public Color Color {get; set;}

    public Square(double radius, Color color)
    {
        this.Radius = radius;
        this.Color = color;
    }
  
    public void Paint()
    {
        // Malt Kreis auf den Bildschirm
    }
}

Nun haben wir zwei tolle Klassen. Nun kommt die Vererbung ins Spiel: Wir sehen, dass der Aufbau beider Klassen dort recht identisch ist. Also können wir eine Grundgerüst - also eine abstrakte Klasse - bauen:

public abstract class Shape
{
    public Color Color {get; set;}
  
    public Square(Color color)
    {
        this.Color = color;
    }
  
    public abstract void Paint();
}


public class Square : Shape
{
    public double Width { get; }
  
    public Square(double width, Color color) : base(color)
    {
        this.Width = width;
    }

    public override void Paint()
    {
        // Malt Quadrat auf den Bildschirm
    }
}

public class Circle : Shape
{
    public double Radius { get; }
  
    public Circle(double radius, Color color) : base(color)
    {
        this.Radius = radius;
    }
  
    public override void Paint()
    {
        // Malt Kreis auf den Bildschirm
    }
}

Square und Circle besitzen nun durch die Vererbung die Eigenschaften von Shape. Mit der abstrakten Paint()-Methode geben wir dann noch zusätzlich an, dass die Methode zwar in den Subklassen zur Verfügung steht aber die konkrete Implementierung in den Klassen Geschehen muss. So wird dann garantiert, dass alle Klassen, die von Shape erben, auch die Paint()-Methode besitzen. Dies ist jetzt nämlich vom Vorteil, denn wir wollen ja geometrische Figuren auf dem Bildschirm zeichnen. Derjenige, der jetzt dafür sorgt, dass die Figur auf dem Bildschirm gezeichnet wird, muss nämlich nicht wissen, ob es nun ein Quadrat oder Kreis ist, sondern muss nur wissen, dass sie vom Typ Shape sind:

public class Screen
{
    public void Paint(Shape shape)
    {
        shape.Paint();
    }
}
Screen screen = new Screen();
Square square = new Square(12.5, Color.RED);
Cirlce circle = new Circle(10, Color.BLUE);

screen.Paint(square);
screen.Paint(circle);

Die Paint()-Methode in der Screen-Klasse muss also nicht den konkreten  Datentyp (Square oder Circle) wissen (ansonsten müssen wir jeden Typ eine eigene Methode schreiben), sondern muss nur wissen, dass die Klasse vom Typ Shape sein muss, um die Paint-Methode aufrufen zu können.

In den Klassen Square und Circle erkennt man auch, dass beide zwar vom Typ Shape sind aber beide haben unterschiedliche Eigenschaften. Einmal Width (Square) und Radius (Circle). Das sind Eigenschaften, die zwar die konkreten Klassen kennen aber Shape nicht:

Shape s1 = new Square(10, Color.BLUE);
Console.WriteLine(s1.Width); // funktioniert nicht, da wir angebenen haben, dass s1 ein Shape-Objekt ist und Shape Width nicht kennt.

Square s2 = s1;
Console.WriteLine(s2.Width); // funktioniert, da s1 ja ein Square ist und wir sagen, dass s2 ein Square ist.

 

Link zu diesem Kommentar
Auf anderen Seiten teilen

@Whiz-zarD

Da bei deinem Beispiel die abstrakte Klasse keine Funktionalität mitbringt auf die die beiden erbenden Klassen zurückgreifen könntest du doch auch ein Interface nutzen im sicher zu gehen das die Klassen eine bestimmte Methode implementieren. 

Wenn sich die Klassen Funktionalität teilen sollen aber eine Methode unterschiedlich Implementiert Wesen soll macht eine Abstrakte Klasse durchaus Sinn da Paint von beiden Klassen anders gehandelt werden muss. 

Sorry für das wirsche schreiben 

Link zu diesem Kommentar
Auf anderen Seiten teilen

vor 9 Stunden schrieb r4phi:

@Whiz-zarD

Da bei deinem Beispiel die abstrakte Klasse keine Funktionalität mitbringt auf die die beiden erbenden Klassen zurückgreifen könntest du doch auch ein Interface nutzen im sicher zu gehen das die Klassen eine bestimmte Methode implementieren. 

Die abstrakte Klasse hat zwar keine Funktionalität, dennoch bräuchte ich neben dem Interface eine abstrakte Klasse, die die Color-Eigenschaft besitzt. Ohne diese abstrakte Klasse müsste ich weiterhin in den konkreten Klassen (Square und Circle) die Eigenschaft implementieren. Mit der abstrakten Klasse habe die Color-Eigenschaft nur ein mal implementiert und spare somit Code und potenzielle Fehlerquellen. 

Bearbeitet von Whiz-zarD
Link zu diesem Kommentar
Auf anderen Seiten teilen

Guten Morgen die Herren und Damen!

Ja, meine Frage bezieht sich auf Vererbung. Dennoch sehr interessant etwas über boxing zu lesen.

Bonusfrage zum Exkurs, wenn ich dynamisch mit Code eine solche Liste mit Objekten befülle die unterschiedlichste Datentypen aufweisen, wie bekomme ich es hin diese Fachgerecht wieder dynamisch zu unboxen? Ist für mich aktuell nicht wichtig aber am Ende vom Tag schlauer zu sein hat noch nicht geschadet.

 

Zurück zum Thema:

Ich muss mich für meinen Code entschuldigen, der ist zwischen Tür und Angel entstanden. Da ist dein Beispiel deutlich schöner. Das Grundlegende Prinzip der Vererbung, warum das eine feine Sache sein kann und warum man von abstrakten Klassen spricht habe ich begriffen denke ich. Dennoch..

Besonders der letzte Part ist für mich von Interesse. Mich interessiert welchen Anwendungsfall es für Shape x = Square gibt. Weil Screen ja auch ein Square y = Square als Shape anerkennt.

Jetzt habe ich meinem FtStudenten noch einen Namen verpasst(ähnlich deinem "Width") und siehe da, ich kann auf den Namen nicht zugreifen. Wie erwartet. Interessant wird es dann aber beim Objekttyp "casten"(ist das casten? Ich bin mir nicht sicher. Keine ganze Typ umwandlung aber irgendwie doch). Das geht bei mir nicht.

Studenten s1 = new FtStudent("Eberhard");

Console.WriteLine(s1.name); //geht nicht. Wie erwartet.

FtStudent s2 = s1; //Das dürfte deinem Square s2 = s1; entsprechen, geht aber nicht.

Console.WriteLine(s2.name); //ohne s2 kommen wir garnicht soweit
           


Fehler    CS1061    "Studenten" enthält keine Definition für "name", und es konnte keine zugängliche name-Erweiterungsmethode gefunden werden, die ein erstes Argument vom Typ "Studenten" akzeptiert (möglicherweise fehlt eine using-Direktive oder ein Assemblyverweis)


Fehler    CS0266    Der Typ "Student.Studenten" kann nicht implizit in "Student.FtStudent" konvertiert werden. Es ist bereits eine explizite Konvertierung vorhanden (möglicherweise fehlt eine Umwandlung)
 

Grüße

 

Bearbeitet von redstav
Link zu diesem Kommentar
Auf anderen Seiten teilen

TipTop. Läuft. Jetzt fehlt mir allerdings immer noch die Kreativität und die Erfahrung warum ich überhaupt jemals Shape x = Square/Circle verwenden sollte... Am Ende zwei "gleiche" Objekte unterschiedlichen Typs zu haben(Shape x1 = Square & Square x2 = Square) ist ja auch nur umständlich und bietet mehr als genug Raum für Fehlerquellen kann ich mir vorstellen.

Bearbeitet von redstav
Link zu diesem Kommentar
Auf anderen Seiten teilen

Angenommen du hast ein Interface, welches eine Datenbankanbindung definiert:

public interface IDbCommandExecuter
{
	public void InsertInto(string tableName, object toInsert);
}

Im Moment hast du eine MySql-Datenbank, sodass du eine Klasse schreibst, welche dieses Interface implementiert und die konkrete Implementierung für die MySql-Datenbank aufweist.

public class MySqlCommandExecuter : IDbCommandExecuter
{
	public void InsertInto(string tableName, object toInsert)
	{
		// MySql Implementierung hier.
	}
}

Jetzt willst du die Methode benutzen und Objekte in die Datenbank einfügen.

...

IDbCommandExecuter exe = new MySqlCommandExecuter();

exe.InsertInto("myTable", myObject1);
exe.InsertInto("myTable", myObject2);
exe.InsertInto("myTable", myObject3);

...

Irgendwann später stellst du jedoch fest, dass MySql nicht mehr optimal ist und du jetzt MariaDB verwenden möchtest.
Du schreibst also eine Klasse, welche dein Interface implementiert und die Implementierung für MariaDB hat:

public class MariaDbCommandExecuter : IDbCommandExecuter
{
	public void InsertInto(string tableName, object toInsert)
	{
		// MariaDB Implementierung hier.
	}
}

Wenn du jetzt MariaDB hernehmen willst, musst du alles nur an einer Stelle ändern, anstatt an vielen verschiedenen.

...

IDbCommandExecuter exe = new MariaDbCommandExecuter();

exe.InsertInto("myTable", myObject1);
exe.InsertInto("myTable", myObject2);
exe.InsertInto("myTable", myObject3);

...

Zusammenfassung:

Da das Interface die Anforderungen definiert, die Klasse jedoch die Implementierung, bist du wenn du ein Interface nutzt von der Implementierung unabhängig. Dies bedeutet, wenn du später die Implementierung ändern willst (MySql -> MariaDB) musst du dies nicht mehr an vielen verschiedenen Stellen machen, sondern nur an einer. Das geht, da du weißt, dass jede Klasse, welche dieses Interface implementiert auch eine Implementierung für diese Methode haben muss.

Das Interface definiert also, was getan werden muss, die Klasse wie es getan werden muss. Und wenn du von außen darauf zugreifst, kann dir das wie ja relativ egal sein.

Bearbeitet von 0x00
Formulierung
Link zu diesem Kommentar
Auf anderen Seiten teilen

Inzwischen schreibt man auch nicht mehr:

Square square = new Square(...);

sondern:

var square = new Square(...);

und überlasst den Kompiler den Rest. Die Angabe, um welchen konkreten Datentypen es sich handelt, ist nur eine doppelte Information, da wir mit new Square(...) schon den Datentyp kennen.

Bearbeitet von Whiz-zarD
Link zu diesem Kommentar
Auf anderen Seiten teilen

Gegenfrage: Hast du wirklich den Sinn von abstrakten Klassen jetzt verstanden?

Es macht wenig Sinn zurück zu abstrakten Klasse zu casten.

Die abstrakte Klasse definiert Methoden und Propterties die deine SubKlasse haben muss bwz. auf die eine Subklasse zugreifen kann.

Manche Methoden sind abstrakt: Bedeutet die SubKlasse muss eine entsprechende Methode dieses Types und mit dem Namen haben...was diese genau macht, ist erstmal egal.

Du kannst auch abstrakte Properties angeben: Heißt die Klasse gibt nur den Namen, den Typ und Getter/Setter an die eine SubKlasse so implementieren muss.

Beispiel:

public abstract class Car{
  
  // Car enthält die Methode navigate...SubKlassen haben darauf Zugriff
  public void navigate(){
    // do stuff
  }
  // Subklassen haben Zugriff auf price, Implementierung nicht möglich, da man die Property schon aus Car bezieht 
  public double Price => 50000;
  
  // SubKlassen müssen eine Property Name vom Typ String implementieren
  public abstract string Name { get; set;}
  // SubKlassen müssen eine Methode drive implementieren
  public abstract void drive();
}

 

Bearbeitet von KeeperOfCoffee
Link zu diesem Kommentar
Auf anderen Seiten teilen

@0x00Sweet! Praktisches Beispiel. Macht Sinn. Merci.

 

@KeeperOfCoffeeInterfaces verstehe ich als Vertrag. Alle Klassen die das Interface implementieren müssen die im Vertrag genannten Punkte erfüllen. Diese sind aber noch nicht konkretisiert. Abstrakte Klassen verstehe ich als abstraktes Konzept für Gruppen. Beispiel: Publikationen(abstrakt) -> Bücher, Magazine, Zeitungen, Artikel... Das Konzept gibt Eigenschaften und Methodiken vor die sich alle Elemente der Gruppe erstmal grundlegend Teilen.

 

@Whiz-zarDIch werde zusehen dass ich mir das angewöhne. Bin von Anfang an um CCD Praktiken bemüht soweit ich die Konzepte begreife, zumindest wenn ich nicht schnell noch die Frage in ein Forum werfen will bevor ich an dem Tag zu nichts mehr komme..^^

 

Abgesehen von der Bonus Frage zum Boxin und Unboxing wars das denke ich. Herzlichen Dank für die Antworten!

Bearbeitet von redstav
Link zu diesem Kommentar
Auf anderen Seiten teilen

Die einzige Möglichkeit welche ich sehe die geboxten Typen dynamisch zu unboxen wäre folgende.

//Liste von Objects erzeugen
List<object> boxedList = new List<object>();
boxedList.Add(2);
boxedList.Add(3);
boxedList.Add(1.1415);

...

foreach(object item in boxedList)
{
	// Type bekommen. Liefert den unterliegenden typ, nicht object!
	string typeName = boxed.GetType().Name;

	// Großes Switch statement
	switch(typeName)
	{
		case nameof(Int16):
			var unboxed = (short)boxed;
			break;
		case nameof(Int32):
			var unboxed = (int)boxed;
			break;
		// Usw.
	}
}

Wüsste aber auch ehrlich gesagt auch nicht, wie man dass sinnvoll nutzen könnte, da der geunboxte Typ nur im Scope des Switch Statement existiert. Gut, da könnte man natürlich auch ein wenig Logik platzieren... Lass mich gerne überraschen, wenn jemand nen guten Nutzen hat^^

Bearbeitet von 0x00
Formulierung
Link zu diesem Kommentar
Auf anderen Seiten teilen

vor 27 Minuten schrieb 0x00:

Wüsste aber auch ehrlich gesagt auch nicht, wie man dass sinnvoll nutzen könnte, da der geunboxte Typ nur im Scope des Switch Statement existiert. Gut, da könnte man natürlich auch ein wenig Logik platzieren... Lass mich gerne überraschen, wenn jemand nen guten Nutzen hat^^

Es ist halt dafür dar, um für Datentypen unterschiedlicher Art einen Eingangspunkt zu ermöglichen. z.B. einer formatierten Ausgabe von Strings:

Console.WriteLine("Hallo {0}, ich bin {1} Jahre alt.", "Welt", 42);

// Ausgabe:
// Hallo Welt, ich bin 42 Jahre alt.

"Welt" ist ein String (Objekt) und 42 ein Integer (Wertetyp). Damit die WriteLine()-Methode mit den unterschiedlichen Datentypen umgehen kann, nimmt sie ein object-Array entgegen (man sieht das Array nicht, wegen dem params-Schlüsselwort). D.h. die 42 wird in ein object konvertiert (boxing). Die Methode nimmt sich dies sogar zum Vorteil, da object die ToString()-Methode besitzt und die 42 kann direkt in ein String umgewandelt werden, da es ja nun ein object ist.

 

Link zu diesem Kommentar
Auf anderen Seiten teilen

vor 11 Minuten schrieb Whiz-zarD:

Es ist halt dafür dar, um für Datentypen unterschiedlicher Art einen Eingangspunkt zu ermöglichen. z.B. einer formatierten Ausgabe von Strings:

Ein anderes gutes Beispiel für die Verwendung ist die gute alte ArrayList:

https://docs.microsoft.com/en-us/dotnet/api/system.collections.arraylist?redirectedfrom=MSDN&view=netframework-4.8

 

vor 4 Stunden schrieb Whiz-zarD:

die Color-Eigenschaft besitzt.

Tatsache - übersehen.
In deinem Beispiel hat die Shape Klasse aber noch einen falschen Konstruktor ;)

 

@0x00

Dein Beispiel sollte auch mit PatternMatching funktionieren.
Müsste ich aber mal ausprobieren.

Zitat
foreach(var obj in boxedList) { 
 switch (o)
    {
        case int16 a :
            cw(a);
            break;
        case int32 b:
            cw(b);
            break;
        default:
            break;
    }
}

 

Bearbeitet von r4phi
Link zu diesem Kommentar
Auf anderen Seiten teilen

Dein Kommentar

Du kannst jetzt schreiben und Dich später registrieren. Wenn Du ein Konto hast, melde Dich jetzt an, um unter Deinem Benutzernamen zu schreiben.

Gast
Auf dieses Thema antworten...

×   Du hast formatierten Text eingefügt.   Formatierung wiederherstellen

  Nur 75 Emojis sind erlaubt.

×   Dein Link wurde automatisch eingebettet.   Einbetten rückgängig machen und als Link darstellen

×   Dein vorheriger Inhalt wurde wiederhergestellt.   Editor leeren

×   Du kannst Bilder nicht direkt einfügen. Lade Bilder hoch oder lade sie von einer URL.

Fachinformatiker.de, 2024 by SE Internet Services

fidelogo_small.png

Schicke uns eine Nachricht!

Fachinformatiker.de ist die größte IT-Community
rund um Ausbildung, Job, Weiterbildung für IT-Fachkräfte.

Fachinformatiker.de App

Download on the App Store
Get it on Google Play

Kontakt

Hier werben?
Oder sende eine E-Mail an

Social media u. feeds

Jobboard für Fachinformatiker und IT-Fachkräfte

×
×
  • Neu erstellen...