Vorsicht bei umfangreichen Strings

  • Allgemein

Es gibt 32 Antworten in diesem Thema. Der letzte Beitrag () ist von Bartosz.

    Bartosz schrieb:

    Die Listbox zeigt den langen String nicht an.
    Wie lang sind denn die Strings?
    ich mein, ein String-Array aus 800KB kann sehr unterschiedlich sein:
    Das kann ein String der Länge 800.000 sein,
    oder 100 Strings, davon 30 mit einer Länge von über 10.000
    oder 5000 Strings, davon 50 mit einer Länge von über 4.000

    also gib mal eine ungefähre Vorstellung: Wie lang sind die längsten, und wieviele gibts davon?



    Zum andern scheint mir das möglich, dass viele überlange Strings die Listbox-Performance kaputtmachen - ich hatte das noch nie, aber denkbar ist sowas.
    Da kann man sich DatenObjekte bauen, mit einer Kurz- und einer Lang-Version. Als Kurzversion nimmste einfach die ersten 50Zch des STrings, und LangVersion den String selbst.
    Dann kannste mit Databinding die Listbox anweisen, nur die Kurz-Version anzuzeigen.
    Die Langversion kann man dann - ebenfalls mit Databinding in einer Textbox anzeigen, welche nur einen String anzeigt, nämlich die Langversion des in der Listbox (als Kurzversion) angewählten Datenobjektes.
    Das einfachste verwendbare DatenObjekt wäre Tuple(Of String, String), das hat die Properties Item1 und Item2 As String.
    Ebensogut verwendbar wäre KeyValuePair(Of String, String), das hat die Properties Key und Value As String.
    Also mach aus deiner Private ReadOnly ListOfUserData As New List(Of String) eine List(Of Tuple(Of String, String)), weise die der Listbox als DataSource zu - anstatt des .AddRange().
    Und setze lb.DisplayMember auf "Item1", und .ValueMember auf "Item2".

    Wenn du mir garnet folgen kannst, stell ein lauffähiges Testprojekt ein, dann kann man das gschwind hinbasteln.
    Hallo ErfinderDesRades,
    im heutigen Testprojekt gibt es einen String mit einer Länge von 818110 Zeichen (s. Textdatei im Anhang aus Post Nr. 19).

    Den Code hänge ich als zip an. Ich verstehe, was du möchtest, allerdings kann ich dir mit dem Binding nicht ganz folgen. Danke für deine Hilfe.
    Dateien

    Bartosz schrieb:

    Das Problem ist die Übertragung an die Oberfläche. Die Listbox zeigt den langen String nicht an.

    Stimmt:
    List boxes store all the strings in the list box in one globally allocated segment. Windows limits the total amount of text in a list box to 64 kilobytes (K).
    List Box Controls
    Mehr als 64K zeigt die Listbox insgesamt nicht an.


    If $"{Convert.ToChar(Data(i))}{Convert.ToChar(Data(i + 1))}{Convert.ToChar(Data(i + 2))}{Convert.ToChar(Data(i + 3))}" = "udta" Then
    Du erzeugst hier Millionen von Strings, wenn die Datei 1 MB hat. Überlegt mal, für jedes Byte in dem Array erzeugst du ein 4 Zeichen langen String, um dann zu schauen, ob dieser = "udta" ist. Natürlich kann dir hier alles explodieren. Das ist doch nicht nötig, wenn du das z.B. einfach so abfragst:

    VB.NET-Quellcode

    1. Dim udtaString As Byte() = System.Text.Encoding.UTF8.GetBytes("udta")
    2. For i As Integer = 0 To Data.Length - 4 Step 1
    3. If Data(i) = udtaString(0) AndAlso Data(i + 1) = udtaString(1) AndAlso Data(i + 2) = udtaString(2) AndAlso Data(i + 3) = udtaString(3) Then
    4. Get_UserData(i)
    5. End If
    6. Next


    Und das gleiche nochmal in deinem Get_UserData. Hier kannst du optimieren.
    hier mein Versuch, entsprechend der Skizze aus post#22:

    VB.NET-Quellcode

    1. Public Class Form1
    2. Private Data As Byte()
    3. Private ReadOnly Latin1 As System.Text.Encoding = System.Text.Encoding.GetEncoding("iso-8859-1")
    4. Private ReadOnly ListOfUserData As New List(Of Tuple(Of String, String))
    5. Private Const _256_3 As UInt32 = 16777216UI
    6. Private Const _256_2 As UInt32 = 65536UI
    7. Public Sub New()
    8. InitializeComponent()
    9. ListBox1.DisplayMember = "Item1"
    10. ListBox1.ValueMember = "Item2"
    11. End Sub
    12. Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    13. Dim path As String = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + "\Testdatei.txt"
    14. path = "..\..\Testdatei.txt"
    15. If Not System.IO.File.Exists(path) Then
    16. Return
    17. End If
    18. Me.Data = System.IO.File.ReadAllBytes(path)
    19. ListOfUserData.Clear()
    20. For i As Integer = 0 To Data.Length - 4 Step 1
    21. If $"{Convert.ToChar(Data(i))}{Convert.ToChar(Data(i + 1))}{Convert.ToChar(Data(i + 2))}{Convert.ToChar(Data(i + 3))}" = "udta" Then
    22. Get_UserData(i)
    23. End If
    24. Next
    25. 'for test: create dummi-userdata
    26. Me.ListOfUserData.Add(Tuple.Create("dummi1", String.Concat(Enumerable.Repeat("dummi1 ", 100))))
    27. Me.ListOfUserData.Add(Tuple.Create("dummi2", String.Concat(Enumerable.Repeat("dummi2_", 100))))
    28. ListBox1.DataSource = Nothing : ListBox1.DataSource = ListOfUserData
    29. End Sub
    30. Private Sub Get_UserData(i As Integer)
    31. Dim boxSize0 As UInt32 = Data(i - 4) * _256_3 + Data(i - 3) * _256_2 + Data(i - 2) * 256UI + Data(i - 1)
    32. For j As Integer = i + 8 To i + CInt(boxSize0) - 8 Step 1 ' nicht 4
    33. If Latin1.GetString({Data(j), Data(j + 1), Data(j + 2), Data(j + 3)}) = "data" Then
    34. Dim boxSize1 As UInt32 = Data(j - 4) * _256_3 + Data(j - 3) * _256_2 + Data(j - 2) * 256UI + Data(j - 1)
    35. 'Dim processedData As Byte() = New Byte(CInt(boxSize1) - 8 - 1) {}
    36. ''Array.Copy(Data, j + 4, processedData, 0, processedData.Length)
    37. 'Using ms As New IO.MemoryStream(Me.Data)
    38. ' ms.Seek(j + 4, IO.SeekOrigin.Begin)
    39. ' ms.Read(processedData, 0, processedData.Length)
    40. 'End Using
    41. ''Dim processedData = Me.Data.Skip(j + 4).Take(CInt(boxSize1) - 8).ToArray
    42. 'Dim obtainedString As String = System.Text.Encoding.UTF8.GetString(processedData).TrimStart({NullChar, Convert.ToChar(1), Convert.ToChar(24)})
    43. ' UTF8 according to the standard
    44. Dim obtainedString = System.Text.Encoding.UTF8.GetString(Me.Data, j + 4, CInt(boxSize1) - 8).TrimStart({NullChar, Convert.ToChar(1), Convert.ToChar(24)})
    45. Dim itm1 = New String(obtainedString.Take(50).ToArray)
    46. Me.ListOfUserData.Add(Tuple.Create(itm1, obtainedString))
    47. j += CInt(boxSize1) - 1 ' Do NOT write "Exit For" because these are single items
    48. End If
    49. Next
    50. End Sub
    51. Private Sub ListBox1_SelectedValueChanged(sender As Object, e As EventArgs) Handles ListBox1.SelectedValueChanged
    52. RichTextBox1.Text = ListBox1.SelectedValue?.ToString
    53. End Sub
    54. End Class
    in der Listbox zur Anzeige kommen nur die Tuple.Item1, welche sind in Länge beschränkt auf 50 (s. Zeile #47)
    Die vollständigen Strings sind dann in beigeordneter Richtextbox angezeigt (zeile #55)
    beachte auch, dass der ganze MemoryStream-Quack üflüssig ist (auskommentiert), wenn man die geeignete Encoding.GetString-Überladung verwendet - zeile #46.
    Dateien
    • WindowsApp300.zip

      (694,95 kB, 231 mal heruntergeladen, zuletzt: )

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „ErfinderDesRades“ ()

    ich hab den Code von post#29 nochma überarbeitet, weil im Auslesen hast du glaub einen Lese-Fehler drin:
    Wenn innerhalb eines "data"-Datenblocks in den RohDaten zufällig mal die Byte-Folge für "udta" oder "data" auftaucht, so wird das bei dir als weiterer Datenblock aufgefasst, und dann iwelcher Mist eingelesen.
    Daher habich jetzt mal was mit ArraySegments gebastelt, wo ein ArraySegment einen DatenContent repräsentiert. So kann man einen Datenblock auslesen, und wenn wolle den nächsten dahinter.
    Und das Lesen des hinteren fängt auch erst danach an - sodass obige Mis-Interpretation vermieden wird.

    Weil das Datenformat der Rohdaten ist ganz pfiffig: Es gibt Datenblöcke mit je einem Header und Content. Der Header besteht aus LängenAngabe (4 Bytes) und Signatur ("udta" oder "data").
    So kann man mehrere gleichartige Datenblöcke hintereinander lesen, aber es können auch innerhalb des einen Datenblocks Datenblöcke eingeschachtelt sein mit anderer Signatur (nämlich zB ein "udta"-Datenblock kann viele "data" enthalten)

    Schlanker und effizienter ist der Code dabei auch geworden:

    VB.NET-Quellcode

    1. Public Class Form1
    2. Private Shared ReadOnly readIntBuffer(3) As Byte
    3. 'Private ReadOnly Latin1 As System.Text.Encoding = System.Text.Encoding.GetEncoding("iso-8859-1")
    4. Private Shared ReadOnly Utf8 As System.Text.Encoding = System.Text.Encoding.UTF8
    5. Private ReadOnly ListOfUserData As New List(Of Tuple(Of String, String))
    6. Public Sub New()
    7. InitializeComponent()
    8. ListBox1.DisplayMember = "Item1"
    9. ListBox1.ValueMember = "Item2"
    10. End Sub
    11. Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    12. Dim path As String = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + "\Testdatei.txt"
    13. path = "..\..\Testdatei.txt"
    14. If Not System.IO.File.Exists(path) Then
    15. Return
    16. End If
    17. Dim fileBytes = System.IO.File.ReadAllBytes(path)
    18. ListOfUserData.Clear()
    19. Dim frame = New ArraySegment(Of Byte)(fileBytes)
    20. Dim udta As ArraySegment(Of Byte) = GetFirstContent(frame, "udta")
    21. While udta <> Nothing
    22. Dim data As ArraySegment(Of Byte) = GetFirstContent(udta, "data")
    23. While data <> Nothing
    24. Dim obtainedString = Utf8.GetString(fileBytes, data.Offset, data.Count).TrimStart({NullChar, Convert.ToChar(1), Convert.ToChar(24)})
    25. Dim itm1 = New String(obtainedString.Take(50).ToArray)
    26. Me.ListOfUserData.Add(Tuple.Create(itm1, obtainedString))
    27. data = GetNextContent(udta, "data", data)
    28. End While
    29. udta = GetNextContent(frame, "udta", udta)
    30. End While
    31. 'for test: create dummi-userdata
    32. Me.ListOfUserData.Add(Tuple.Create("dummi1", String.Concat(Enumerable.Repeat("dummi1 ", 100))))
    33. Me.ListOfUserData.Add(Tuple.Create("dummi2", String.Concat(Enumerable.Repeat("dummi2_", 100))))
    34. ListBox1.DataSource = Nothing : ListBox1.DataSource = ListOfUserData
    35. End Sub
    36. Private Sub ListBox1_SelectedValueChanged(sender As Object, e As EventArgs) Handles ListBox1.SelectedValueChanged
    37. RichTextBox1.Text = ListBox1.SelectedValue?.ToString
    38. End Sub
    39. ''' <summary>liest aus arr einen Integer an Position i</summary>
    40. Private Shared Function ReadInt(arr As Byte(), i As Integer) As Integer
    41. Array.Copy(arr, i, readIntBuffer, 0, 4)
    42. Array.Reverse(readIntBuffer)
    43. Return BitConverter.ToInt32(readIntBuffer, 0)
    44. End Function
    45. ''' <summary>sucht in frame nach dem ersten Datenblock, dessen Header auf key matcht.</summary>
    46. Private Shared Function GetFirstContent(frame As ArraySegment(Of Byte), key As String) As ArraySegment(Of Byte)
    47. Return GetNextContent(frame, key, New ArraySegment(Of Byte)(frame.Array, frame.Offset, 0))
    48. End Function
    49. ''' <summary>sucht in frame nach dem ersten Datenblock, dessen Header auf key matcht, und der hinter previous liegt.</summary>
    50. Private Shared Function GetNextContent(frame As ArraySegment(Of Byte), key As String, previous As ArraySegment(Of Byte)) As ArraySegment(Of Byte)
    51. Dim arr = frame.Array
    52. Dim pattern = Utf8.GetBytes(key)
    53. For i = previous.Offset + previous.Count + 4 To frame.Offset + frame.Count
    54. If pattern.SequenceEqual(Enumerable.Range(i, pattern.Length).Select(Function(x) arr(x))) Then 'pattern-match?
    55. Dim contentSize = ReadInt(arr, i - 4) - (4 + pattern.Length)
    56. Return New ArraySegment(Of Byte)(arr, i + pattern.Length, contentSize)
    57. End If
    58. Next
    59. Return Nothing
    60. End Function
    61. End Class

    Dieser Beitrag wurde bereits 4 mal editiert, zuletzt von „ErfinderDesRades“ ()

    Vielen lieben Dank für deine Ausarbeitung.
    ArraySegment ist neu für mich. Danke dafür. Mir fällt auch auf, dass man nicht mehr auf Indizes (Data(i)) angewiesen ist und damit eine Datei, die größer als 2GB ist, lesen kann. Man bräuchte dann nur eine Implementierung für alle Boxen, also nicht nur udta. Aber darum geht es hier nicht.
    Ich setze den Thread auf erledigt.