Private/Wissen/B_Basic/B41_Typen.ps1

<#
 
# PowerShell und .NET Typen
 
Die Bedeutung und Verwendung von Typen in der PowerShell.
 
- **Hashtags** Werttyp Referenztyp Hashtable int long double decimal datetime array hex xml StringData Email single ScriptBlock
 
- **Version** 2021.10.1
 
#>


[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')]
param()

# ! Sämtliche Typen der PowerShell stammen aus .NET oder wurden von solchen Typen abgeleitet.

# ! In der PowerShell gibt es für einige Typen Aliase und stellen ein Synonym zum .NET-Typen dar, z.B.:

[int] # Alias für:
[Int32] # ! KEIN Alias
[System.Int32]

[long]
[Int64]
[System.Int64]

# ! Die wichtigsten PowerShell Typen in der Übersicht:

[PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')::Get.Keys

# TIPP - HANDOUT:
('DIE WICHTIGSTEN¹ POWERSHELL-TYPEN' + [System.Environment]::NewLine + '=================================' + ([PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')::Get.Keys | Sort-Object | ForEach-Object { [PSCustomObject]@{ TypeName = "[ ] $_" } } | Format-Wide -Column 2 | Out-String -Width 100) + [System.Environment]::NewLine + '¹) Typen abhacken deren Bedeutung erkennbar ist, benutzt wurden oder angelesen wurde.') -split '\r\n' -replace "\s+$", [string]::Empty | Out-Printer



#region Namespace

# .NET Typen sind in Namespace organisiert. Der Namespace System muss nicht angegeben werden. Typen die aus diesem Namespace werden automatisch gefunden:

[System.Int32]
[Int32]

# Die Angabe des Namespace kann jedoch für die Autovervollständigung hilfreich sein:

[System.Int32]

# Typen die nicht im Namespace System liegen MÜSSEN absolut angesprochen werden:

[Stopwatch] # ! Der Typ [StopWatch] wurde nicht gefunden.
[System.Diagnostics.Stopwatch]

# TIPP - Wenn Sie Typen aus einem nicht bekannten Namespace oft benötigen, können Sie diesen Namespace in die Session importieren:
using namespace System.Diagnostics # ! WICHTIG muss am Anfang des Skriptes stehen.
[Stopwatch]

# ! Achtung, Typen können nur genutzt werden, wenn die dazugehörige .DLL-Datei (Assembly) geladen wurde:
[System.Windows.Forms.MessageBox]::Show('Hallo Würzburg!') # ! Der Typ [System.Windows.Forms.MessageBox] wurde nicht gefunden.
Add-Type -AssemblyName 'System.Windows.Forms'
[System.Windows.Forms.MessageBox]::Show('Hallo Würzburg!')

#endregion

#region String (Text)

# ! Ein String-Argument kann ohne diesen in Sonderzeichen einzuschließen zu müssen, einem Parameter übergeben werden solange dieser Text keine Leerstellen enthält:

Get-ChildItem -Path C:\temp

# ! String-Wert wird der Variable $Ort zugewiesen:

$Ort = 'Würzburg'

# ! Excl. Variablen-Auflösung:

'Begrüßung $Ort!'

# ! Incl. Variablen-Auflösung:

"Hallo $Ort!"

# ! Incl. Ausdruck-Auflösung:

"Heute ist $(Get-Date -Format 'dddd')!"
$Ort.Length
"Der Ortsname $Ort ist $($Ort.Length) Zeichen lang!"

# ! String-Block incl. Sonderzeichen:

$StringBlockBeispiel = @"
Dieser String-Block kann "Doppelte Anführungszeichen",
Zeilenumbrüche oder sonstige Steuer Zeichen wie ein Tabulatur enthalten!
 
    - Die erste Zeile MUSS mit @" enden!
    - Die letzte Zeile muss mit "@ beginnen!
 
Erstellt in "$Ort", am $([DateTime]::Today)
"@


# ! Escape characters, Delimiters and Quotes (http://ss64.com/ps/syntax-esc.html):

"Hallo `n Köln!"
"Hallo `r`n Köln!" # Carriage return + New line
"Hallo `t Köln!"   # Horizontal tab

# TIPP - Sonderzeichen in der PowerShell:
Get-Help -Name 'about_Special_Characters' -ShowWindow

#endregion

#region Byte, Int16, UInt16, int, Int32, UInt32, long, Int64 und UInt64 (Ganzzahl)

# ! Verwendung:

$Ganzzahl = 10 # ? Default: Int32, int
$Ganzzahl = [byte]10
$Ganzzahl = [long]10

# ! Wertbereiche:

[Byte]::MinValue
[Byte]::MaxValue

[Int16]::MinValue
[Int16]::MaxValue

[UInt16]::MinValue
[UInt16]::MaxValue

# ? Alias: int
[Int32]::MinValue
[Int32]::MaxValue

[UInt32]::MinValue
[UInt32]::MaxValue

# ? Alias: long
[Int64]::MinValue
[Int64]::MaxValue

[UInt64]::MinValue
[UInt64]::MaxValue

# ! Hexadezimale Werte:

'{0:X}' -f 255
# oder:
0xFF
# oder:
[Convert]::ToByte(0xFF)
# oder:
255 | Format-Hex

#endregion

#region Decimal (Dezimalzahlen), Double, Single (Gleitkommazahlen)

# ! Verwendung:

$Gleitkommazahlen = 10.42 # ? Default: double
$Gleitkommazahlen = [Decimal]10.42

# ! Wertbereiche:

[Decimal]::MinValue
[Decimal]::MaxValue
[Decimal] | Get-Member -Static

[Double]::MinValue
[Double]::MaxValue
[Double] | Get-Member -Static

[Single]::MinValue
[Single]::MaxValue
[Single] | Get-Member -Static

#endregion

#region DateTime, DateTimeOffset (Datum und Zeit)

# ! Wertbereiche:

[DateTime]::MinValue
[DateTime]::MaxValue
[DateTime] | Get-Member -Static

# incl. Zeitzone +/-14h
[DateTimeOffset]::MinValue
[DateTimeOffset]::MaxValue
[DateTimeOffset] | Get-Member -Static

# ! Verwendung:

$DatumZeit = [datetime]'04/23/2020' # ? Default: en-US => MM/dd/yyyy
$DatumZeit = [datetime]'01.10.2020' # ? Default: en-US => MM/dd/yyyy

# ? String in DateTime konvertieren:

# OS Ländereinstellung:
[Convert]::ToDateTime('1.10.2019')

# ISO:
[Convert]::ToDateTime('2019-10-1')

# en-US:
[Convert]::ToDateTime('10/1/2019', [CultureInfo]'en-US')

# de-DE:
'01.10.2020'.ToDateTime([CultureInfo]"de-DE")

# yyyyMMdd:
[DateTime]::ParseExact('20200126', 'yyyyMMdd', $null)

# Benutzereingabe anhand dessen OS-Sprache parsen:
$ci = Get-Culture
$benutzereingabe = Read-Host -Prompt "Bitte Datum eingeben ($($ci.DateTimeFormat.ShortDatePattern))"
$benutzereingabe | Get-Member
$datum = $benutzereingabe.ToDateTime($ci)
$datum | Get-Member
$datum.AddDays(-32)

# ? Unix-Time:

$unixtime = Get-ItemProperty -Path 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion' | Select-Object -ExpandProperty 'InstallDate'
$span = New-TimeSpan -Seconds $unixtime
$start = [datetime]'1970-01-01'
$start + $span

# ? ISO Wochennummer:

Get-Date -UFormat '%V'

# ? (Aktuelles) Datum in einem besonderem Format zurück geben:

Get-Date -Format 'ddddd, d. MMMM yyyy'
'{0:dddd, d. MMMM yyyy}' -f [datetime]'2020-05-13'

$heute = Get-Date
'Heute ist der {0:dd}., Morgen ist {1:ddd}. und Übermorgen ist {2:dddd}.' -f $heute, $heute.AddDays(1), $heute.AddDays(2)

#endregion

#region TimeSpan (Zeitdauer)

# Tick:
[TimeSpan]1

# oder Tage:
[timespan]5d

# Minute:
New-TimeSpan -Minutes 1 -Seconds 30

# Heute bis 31. Dez. 2020:
New-TimeSpan -End '2021-12-31'

# z.B.:
$cmdletDauer = Measure-Command -Expression { Get-ChildItem -Path C:\ -Directory -Recurse -ErrorAction Ignore | Select-Object -First 1000 }
$cmdletDauer | Get-Member
$cmdletDauer.TotalSeconds

#endregion

#region Array

# ? Eindimensionales Array zuweisen:

$eda = 1, 2, 3, 4, 5, 6
$eda = 1..1000
$eda[500]

$processes = Get-Process | Select-Object -ExpandProperty Name # Es gibt mehr als 1 Prozess => Array
$processes[1]
$processes[$processes.Count - 1]
$processes | Select-Object -Last 1

$process = Get-Process -Name WinLogOn | Select-Object -ExpandProperty Name # Es gibt nur 1 Prozess => String
$process.Length   # => Anzahl der Zeichen aus dem Prozess-Namen => Variable ist vom Typ System.String
$processes.Length # => Anzahl der Elemente im Array => Variable ist vom Typ System.Array

$process.GetType()
$processes.GetType()

# ? Zwei-/Mehrdimensionales Array zuweisen:

$mda = New-Object -TypeName ‘Int32[, ]’ -ArgumentList 2, 2
$mda = (11,12,13), (21,22,23), (31,32,33)
$mda[1][1]
$mda[$mda.Length-1]

# ? Gemischtes Array zuweisen:

$ga = 'Würzburg', 12, (Get-Process)
$ga.Length
$ga | Select-Object -Last 1
$ga[2][0]

# ! Array-Operatoren:

# ? Filtern (lt le ne eq ge gt):
10, 11, 12, 10 -eq 10

# ? Prüfen:
10, 11, 12, 10 -contains 10
10 -in 10, 11, 12, 10

# ? Leeres Array erstellen und befüllen:
$x = @()
$x += Get-Date -Format 'hhmmssff'
$x.Length
$x

# ! ACHTUNG !!! AN EIN ARRAY KÖNNEN KEINE NEUE ELEMENTE ANGEFÜGT WERDEN:

$varC = @()
Measure-Command { 1..20000 | ForEach-Object { $varC += "Objekt $_" } } | Select-Object TotalSeconds
$varC.Length
$varC[500]

# ! VS.

$varD = New-Object -TypeName 'System.Collections.ArrayList'
Measure-Command { 1..20000 | ForEach-Object { $varD.Add("Objekt $_") | Out-Null } } | Select-Object TotalSeconds
$varD.Count
$varD[500]

# ! Um die Lesegeschwindigkeit zu höhen, einfach eine ArrayList in ein Array umwandeln:
$varE = $varD.ToArray()

#endregion

#region ScriptBlock

# ! Ein Skriptblock ist eine Sammlung von Anweisungen oder Ausdrücken die als einzelne Einheit verwendet werden kann. Ein ScriptBlock hat die Aufgabe den Definitionszeitpunkt vom Ausführunszeitpunkt zu entkoppeln. Ein Skriptblock kann Argumente akzeptieren und Werte zurückgeben.

# READ Weiterführende und Nachschlage-Informationen:

Get-Help -Name 'about_script_blocks' -ShowWindow

# ! Anwendung:

# Definieren:
$sc = { Get-Process }
$sc | Get-Member

# Inhalt Script-Block anzeigen:
$sc

# Script-Block ausführen:
. $sc

# READ [ScriptBlock] werden oft als Typ für Cmdlet-Parameter benutzt um Code und Ausführungszeit zu trennen:

Get-Command -ParameterType [ScriptBlock] # ! Aktuell geladene Module

# ! Beispiele:

Invoke-Command -ComputerName 'ServerX' -ScriptBlock { Get-Process }

Get-Process | Where-Object -FilterScript { $_.Company -like 'Microsoft*' -or $_.Company -like 'Oracle*' }

Get-ChildItem -Path 'C:\Temp' -File | ForEach-Object -Process {
    $_ | Add-Member -Name 'Besitzer' -Value ( $_ | Get-Acl | Select-Object -ExpandProperty 'Owner' ) -MemberType 'NoteProperty' -PassThru
}

#endregion

#region Hashtable

# ! Ein Hashtable ist ein besondere Collection (System.Collections.Hashtable), ähnlich eines Arrays deren Werte nicht über eine Indexnummer angesprochen werden sondern über einen eindeutigen Namen ('Hash')
# ! - Der Hashname kann frei gewählt werden MUSS aber im Hashtable eindeutig sein.
# ! - Zu jedem Hashnamen muss ein Value zugewiesen werden. Alle Objekte können als Value verwendet werden.
# ! - Beliebige viel Hashname-Value-Paare können in EINEM Hashtable enthalten sein.

# READ Weiterführende und Nachschlage-Informationen:

Get-Help -Name 'about_Hash_Tables' -ShowWindow

# ! Syntax:

@{ HashNameA = "Wert" ; HashNameB = "Wert" }
# oder:
@{
    # TIPP Zeilenumbrüche sind erlaubt, dann ist ein abschließendes ';' nicht nötig.
    HashNameA = "Wert"
    HashNameB = "Wert"
}

# ! FEHLER da der Hash-Name mehrdeutig ist:
@{ HashNameA = "Wert" ; HashNameA = "Wert" }

# ! Verwendungszweck 0)

$ht = @{ HashNameA = "Wert A" ; HashNameB = "Wert B" }
$ht['HashNameB']

# ! Verwendungszweck A): Um neue oder alt Eigenschaften an vorhanden Objekten anzuhängen bzw. zu überschreiben:

@{ Label = "" ; Expression = {} } # => @{ Label = "Label ist der Name der Eingenschaft"; Expression = { Expression enthält die Formel die den Werte der Eigenschaft zurück gibt } }

# ? z.B. Dateigröße in KB anzeigen bzw. mit KB weiter arbeiten (filtern, sortieren, etc.):
Get-ChildItem -Path 'c:\Windows' -File | Select-Object -Property 'Name', @{Label = 'LengthKB' ; Expression = {                  $_.Length / 1KB     } }
Get-ChildItem -Path 'c:\Windows' -File | Select-Object -Property 'Name', @{Label = 'LengthKB' ; Expression = { [int](           $_.Length / 1KB   ) } }
Get-ChildItem -Path 'c:\Windows' -File | Select-Object -Property 'Name', @{Label = 'LengthKB' ; Expression = { [Math]::Ceiling( $_.Length / 1KB   ) } }
Get-ChildItem -Path 'c:\Windows' -File | Select-Object -Property 'Name', @{Label = 'LengthKB' ; Expression = { [Math]::Round(   $_.Length / 1KB, 1) } }
Get-ChildItem -Path 'c:\Windows' -File | Select-Object -Property 'Name', @{Label = 'LengthKB' ; Expression = { '{0,8:0.0}' -f ( $_.Length / 1KB   ) } } # ! NACHTEIL: Output ist IMMER String

# ? z.B. Übersicht mit Dateiname, Dateigröße und Besitzer anzeigen/arbeiten:
Get-ChildItem -Path 'c:\Temp' -File
Get-ChildItem -Path 'c:\Temp' -File | Get-Acl
Get-ChildItem -Path 'c:\Temp' -File | Select-Object -Property Name, 'Length', @{ Label = 'Besitzer'; Expression = { $_ | Get-Acl | Select-Object -ExpandProperty 'Owner' } }

# ! Verwendungszweck B) Ein komplett neues Objekte mit eigenen definierten Eigenschaften erzeugen:

# ? z.B. Übersicht mit Dateiname, Dateigröße und Besitzer anzeigen/arbeiten:
Get-ChildItem -Path 'c:\Temp' -File | ForEach-Object -Process {
    [PSCustomObject]@{
        Dateiname    = $_.Name
        DateigrößeKB = [int]($_.Length / 1KB)
        Besitzer     = $_ | Get-Acl | Select-Object -ExpandProperty 'Owner'
    }
} | Get-Member

# ! ... oder ein Hashtable durch Benutzung erstellen:
$person = @{}
$person.Age = 23
$person.Name = 'Tobias'
$person.Status = 'Online'
[PsCustomObject]$person | Get-Member

# ! Verwendungszweck C) Splatting:

Get-Help -Name 'about_Splatting' -ShowWindow

New-SelfSignedCertificate -Subject 'CN=_FirstName_LastName (PS Developer), E=v.nachname@abc.local' -HashAlgorithm 'SHA512' -KeyExportPolicy [Microsoft.CertificateServices.Commands.KeyExportPolicy]::ExportableEncrypted -CertStoreLocation 'Cert:\CurrentUser\My' -Type [Microsoft.CertificateServices.Commands.CertificateType]::CodeSigningCert -NotAfter (Get-Date).AddYears(5)
# ! vs.
$params = @{
    Subject           = 'CN=_FirstName_LastName (PS Developer), E=v.nachname@abc.local'
    HashAlgorithm     = 'SHA512'
    KeyExportPolicy   = [Microsoft.CertificateServices.Commands.KeyExportPolicy]::ExportableEncrypted
    CertStoreLocation = 'Cert:\CurrentUser\My'
    Type              = [Microsoft.CertificateServices.Commands.CertificateType]::CodeSigningCert
    NotAfter          = (Get-Date).AddYears(5)
}
New-SelfSignedCertificate @params # ! Aus $ muss @ werden

#endregion

#region Enumeration (Enum)

# Enumerations (Enums) ist eine Untermenge von Type. Ein Enum bietet die Möglichkeiten eine Zahl mit einem Schlüsselwort zu verbinden und mehrere solcher Verbindungen unter einem Namen zu Kategorisieren.
# Das Anwendungsgebiet von Enums ist i.d.R. bei Properties / Variablen zu finden die nur vorgegeben Werte enthalten dürfen, z.B.:

$ErrorActionPreference -is [Enum]                                              # => Ist eine Enum
$ErrorActionPreference | Get-Member                                            # => TypeName: System.Management.Automation.ActionPreference-Enumeration
$ErrorActionPreference                                                         # => Schlüsselwort Continue
[Int32]$ErrorActionPreference                                                  # => Zugehörige Zahlenwert 2
$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop # => Stop => 1

# TIPP Welche Bedeutung ein Schlüsselwort hat kann ergooglet werden, z.B. siehe 1. Treffer unter https://www.google.com/search?q=System.Management.Automation.ActionPreference

# ! WICHTIG - Wenn ein Enum an einem Parameter übergeben wird IMMER in ( )-Klammern setzen!

# TODO Eigene Enum erstellen und als Variable oder Property verwenden:

enum ComputerStatus {
    Unbekannt
    An
    Aus
}

[ComputerStatus]$AktuellerStatus = [ComputerStatus]::An
if($AktuellerStatus -ne [ComputerStatus]::Unbekannt) {
    'Geht es weiter...' | Write-Warning
}

# TIPP Mit Get-Enum aus dem Modul AKPT arbeiten:
Import-Module -Name '..\AKPT'
Get-Enum -Value 'Continue' # ? Welche Enums enthalten ein Schlüsselwert 'Continue'
Get-Enum -Name 'Action'    # ? Welche Enums enthalten im Typename das Wort 'Action'
Get-Enum -All              # ? Welche Enums gibt es per Default

# ÜBUNG - Enum - A: Welche Dienst laufen aktuell nicht?

# ÜBUNG - Enum - B: Welches Startverhalten der Dienst steht auf Manuell oder Automatisch?

#endregion

#region StringData

# ! Mittels Data Section können Textzeichenfolgen oder andere schreibgeschützte Daten von der Skriptlogik isoliert werden.

# READ Weiterführende und Nachschlage-Informationen:

Get-Help -Name 'about_Data_Sections' -ShowWindow

# ? Beispiel:

$LocalizedData = Data {
    ConvertFrom-StringData @'
    CannotDetermineTestName = Cannot determine test name
    InvalidTestInfo = TestInfo must contain the path and the list of tests
    InspectingModules = Inspecting Modules
    DiagnosticSearch = Searching for Diagnostics in {0}
    InvokingTest = Invoking tests in {0}
'@

}
$LocalizedData.InvalidTestInfo
$LocalizedData.InvokingTest

#endregion

#region EMail

$emailAdresse = 'info@attilakrick.com'
$emailAdresse = [System.NET.Mail.MailAddress]'Attila Krick<info@attilakrick.com>' # TIPP: inkl. Syntax-Prüfung
$emailAdresse.Address
$emailAdresse.DisplayName
$emailAdresse.User
$emailAdresse.Host

#endregion

#region XML

# ! Definition:

$exchange = [xml]@"
<gesmes:Envelope xmlns:gesmes="http://www.gesmes.org/xml/2002-08-01" xmlns="http://www.ecb.int/vocabulary/2002-08-01/eurofxref">
    <gesmes:subject>Reference rates</gesmes:subject>
    <gesmes:Sender>
        <gesmes:name>European Central Bank</gesmes:name>
    </gesmes:Sender>
    <Cube>
        <Cube time="2020-05-22">
            <Cube currency="USD" rate="1.0904"/>
            <Cube currency="JPY" rate="117.26"/>
            <Cube currency="BGN" rate="1.9558"/>
        </Cube>
        <Cube time="2020-05-21">
            <Cube currency="USD" rate="1.1"/>
            <Cube currency="JPY" rate="118.42"/>
            <Cube currency="BGN" rate="1.9558"/>
        </Cube>
    </Cube>
</gesmes:Envelope>
"@


# ! Verwendung:
[decimal]$rate = $exchange.Envelope.Cube.Cube | Where-Object time -EQ '2020-05-22' | Select-Object -ExpandProperty 'Cube' | Where-Object currency -eq 'USD' | Select-Object -ExpandProperty 'rate'
$rate * 100

# ! Beispiel für XPath und Schematas:

$xNamespaces = @{
    gesmes = 'http://www.gesmes.org/xml/2002-08-01'
    ecb    = 'http://www.ecb.int/vocabulary/2002-08-01/eurofxref'
}
$xDocument = [xml](Invoke-WebRequest -Uri 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml' | Select-Object -ExpandProperty 'Content')
$xInfo = $xDocument | Select-Xml -XPath "/gesmes:Envelope/ecb:Cube/ecb:Cube[@time='2020-05-22']" -Namespace $xNamespaces
$xInfo.Node.Cube

# ! XML-Daten auswerten:

[xml]$xDocument = Invoke-WebRequest -Uri 'http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml' | Select-Object -ExpandProperty 'Content'
[decimal]$rate = $xDocument.Envelope.Cube.Cube.Cube | Where-Object -Property 'currency' -EQ -Value 'AUD' | Select-Object -ExpandProperty 'rate'
$rate * 100

# ! In eine XML-Struktur schreiben:

[xml]$config = @"
<?xml version="1.0" encoding="utf-8" ?>
<Einstellungen>
    <Server Name="ADC01" IP="127.0.0.1" />
    <Server Name="ADC02" IP="127.0.0.1" />
</Einstellungen>
"@

($config.Einstellungen.Server  | Where-Object -Property 'Name' -Like -Value '*02').IP = '192.168.50.32'
$config.Save('C:\Temp\einstellung.xml')
[xml]$result = Get-Content 'C:\Temp\einstellung.xml'
$result.Einstellungen.Server

#endregion

#region Eigene Datentypen definieren

Get-Help -Name 'about_classes' -ShowWindow

class Wechselkurs {
    [ValidatePattern('^[A-Z]{3,3}$')]
    [string]$Währung

    [ValidateRange(0, 10000)]
    [decimal]$Rate
}
$objekt = New-Object -TypeName Wechselkurs -Property @{Währung = 'USD'; Rate = 1.4567}
$objekt | Get-Member
$objekt.Währung = "AA"
$objekt.Rate = -1

#endregion

#region Werttypen vs. Referenztype

# ! Werttypen sind primitive Typen:

$a = 10
$b = 20
$a = $b # <- ACHTUNG
$b = 99
'$a = {0} ; $b = {1}' -f $a, $b
[Object]::ReferenceEquals($a, $b)
$b.GetType().IsPrimitive

# ! Referenztype:

$a = @(10)
$b = @(20)
$a = $b # <- ACHTUNG
$b[0] = 99
'$a = {0} ; $b = {1}' -f $a[0], $b[0]
[Object]::ReferenceEquals($a, $b)
$b.GetType().IsPrimitive

# ? Wert- oder Referenztype:

$a = [datetime]'2019-01-11' - [datetime]'2019-01-01'
$b = [datetime]'2019-01-21' - [datetime]'2019-01-01'
$a = $b
'$a = {0} ; $b = {1}' -f $a[0], $b[0]
[Object]::ReferenceEquals($a, $b)
$a.PSTypeNames
$b.GetType().IsPrimitive
$b.GetType().IsValueType

#endregion

# TODO QUIZ - https://attilakrick.com/schlagwort/powershell-objekte/