├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── img ├── FixedStrings.png └── FixedStrings.svg ├── license.txt ├── readme.md └── src ├── FixedStrings.Benchmarks ├── FixedStrings.Benchmarks.csproj └── Program.cs ├── FixedStrings.Tests ├── FixedStrings.Tests.csproj └── TestFixedStrings.cs ├── FixedStrings.sln ├── FixedStrings ├── FixedStrings.csproj ├── FixedStrings.g.cs ├── FixedStrings.tt └── IFixedString.cs ├── dotnet-releaser.toml └── global.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome:http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # All Files 7 | [*] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 4 11 | insert_final_newline = false 12 | trim_trailing_whitespace = true 13 | csharp_using_directive_placement = outside_namespace:silent 14 | csharp_prefer_simple_using_statement = true:suggestion 15 | csharp_prefer_braces = true:silent 16 | csharp_style_namespace_declarations = block_scoped:silent 17 | csharp_style_prefer_method_group_conversion = true:silent 18 | csharp_style_prefer_top_level_statements = true:silent 19 | csharp_style_expression_bodied_methods = false:silent 20 | csharp_style_expression_bodied_constructors = false:silent 21 | csharp_style_expression_bodied_operators = false:silent 22 | csharp_style_expression_bodied_properties = true:silent 23 | csharp_style_expression_bodied_indexers = true:silent 24 | csharp_style_expression_bodied_accessors = true:silent 25 | csharp_style_expression_bodied_lambdas = true:silent 26 | csharp_style_expression_bodied_local_functions = false:silent 27 | csharp_style_throw_expression = true:suggestion 28 | csharp_style_prefer_null_check_over_type_check = true:suggestion 29 | csharp_prefer_simple_default_expression = true:suggestion 30 | csharp_style_prefer_local_over_anonymous_function = true:suggestion 31 | csharp_style_prefer_index_operator = true:suggestion 32 | csharp_style_prefer_range_operator = true:suggestion 33 | csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion 34 | csharp_style_prefer_tuple_swap = true:suggestion 35 | csharp_style_prefer_utf8_string_literals = true:suggestion 36 | csharp_style_inlined_variable_declaration = true:suggestion 37 | csharp_indent_labels = one_less_than_current 38 | csharp_style_deconstructed_variable_declaration = true:suggestion 39 | dotnet_diagnostic.NUnit2006.severity = silent 40 | dotnet_diagnostic.NUnit2005.severity = silent 41 | dotnet_diagnostic.NUnit2004.severity = silent 42 | dotnet_diagnostic.NUnit2003.severity = silent 43 | dotnet_diagnostic.NUnit2002.severity = silent 44 | dotnet_diagnostic.NUnit2001.severity = silent 45 | 46 | # Solution Files 47 | [*.sln] 48 | indent_style = tab 49 | 50 | # XML Project Files 51 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 52 | indent_size = 2 53 | 54 | # Configuration Files 55 | [*.{json,xml,yml,config,props,targets,nuspec,resx,ruleset}] 56 | indent_size = 2 57 | 58 | # Txt/Markdown Files 59 | [*.{md,txt}] 60 | trim_trailing_whitespace = false 61 | 62 | # Web Files 63 | [*.{htm,html,js,ts,css,scss,less}] 64 | indent_size = 2 65 | insert_final_newline = true 66 | 67 | # Bash Files 68 | [*.sh] 69 | end_of_line = lf 70 | 71 | [*.{cs,vb}] 72 | #### Naming styles #### 73 | 74 | # Naming rules 75 | 76 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 77 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 78 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 79 | 80 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 81 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 82 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 83 | 84 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 85 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 86 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 87 | 88 | # Symbol specifications 89 | 90 | dotnet_naming_symbols.interface.applicable_kinds = interface 91 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 92 | dotnet_naming_symbols.interface.required_modifiers = 93 | 94 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 95 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 96 | dotnet_naming_symbols.types.required_modifiers = 97 | 98 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 99 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 100 | dotnet_naming_symbols.non_field_members.required_modifiers = 101 | 102 | # Naming styles 103 | 104 | dotnet_naming_style.begins_with_i.required_prefix = I 105 | dotnet_naming_style.begins_with_i.required_suffix = 106 | dotnet_naming_style.begins_with_i.word_separator = 107 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 108 | 109 | dotnet_naming_style.pascal_case.required_prefix = 110 | dotnet_naming_style.pascal_case.required_suffix = 111 | dotnet_naming_style.pascal_case.word_separator = 112 | dotnet_naming_style.pascal_case.capitalization = pascal_case 113 | 114 | dotnet_naming_style.pascal_case.required_prefix = 115 | dotnet_naming_style.pascal_case.required_suffix = 116 | dotnet_naming_style.pascal_case.word_separator = 117 | dotnet_naming_style.pascal_case.capitalization = pascal_case 118 | dotnet_style_coalesce_expression = true:suggestion 119 | dotnet_style_null_propagation = true:suggestion 120 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 121 | dotnet_style_prefer_auto_properties = true:silent 122 | dotnet_style_object_initializer = true:suggestion 123 | dotnet_style_collection_initializer = true:suggestion 124 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 125 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 126 | dotnet_style_prefer_conditional_expression_over_return = true:silent 127 | dotnet_style_explicit_tuple_names = true:suggestion 128 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 129 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 130 | dotnet_style_prefer_compound_assignment = true:suggestion 131 | dotnet_style_prefer_simplified_interpolation = true:suggestion 132 | dotnet_style_namespace_match_folder = true:suggestion 133 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 134 | tab_width = 4 135 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | *.sh text eol=lf -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'doc/**' 7 | - 'img/**' 8 | - 'changelog.md' 9 | - 'readme.md' 10 | pull_request: 11 | 12 | jobs: 13 | build: 14 | uses: xoofx/.github/.github/workflows/dotnet.yml@main 15 | secrets: 16 | NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} 17 | PAT_GITHUB_TOKEN: ${{ secrets.PAT_GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # Rider 14 | .idea/ 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Ww][Ii][Nn]32/ 30 | [Aa][Rr][Mm]/ 31 | [Aa][Rr][Mm]64/ 32 | build/ 33 | bld/ 34 | [Bb]in/ 35 | [Oo]bj/ 36 | [Ll]og/ 37 | [Ll]ogs/ 38 | 39 | # Visual Studio 2015/2017 cache/options directory 40 | .vs/ 41 | # Uncomment if you have tasks that create the project's static files in wwwroot 42 | #wwwroot/ 43 | 44 | # Visual Studio 2017 auto generated files 45 | Generated\ Files/ 46 | 47 | # MSTest test Results 48 | [Tt]est[Rr]esult*/ 49 | [Bb]uild[Ll]og.* 50 | 51 | # NUnit 52 | *.VisualState.xml 53 | TestResult.xml 54 | nunit-*.xml 55 | 56 | # Build Results of an ATL Project 57 | [Dd]ebugPS/ 58 | [Rr]eleasePS/ 59 | dlldata.c 60 | 61 | # Benchmark Results 62 | BenchmarkDotNet.Artifacts/ 63 | 64 | # .NET Core 65 | project.lock.json 66 | project.fragment.lock.json 67 | artifacts/ 68 | 69 | # ASP.NET Scaffolding 70 | ScaffoldingReadMe.txt 71 | 72 | # StyleCop 73 | StyleCopReport.xml 74 | 75 | # Files built by Visual Studio 76 | *_i.c 77 | *_p.c 78 | *_h.h 79 | *.ilk 80 | *.meta 81 | *.obj 82 | *.iobj 83 | *.pch 84 | *.pdb 85 | *.ipdb 86 | *.pgc 87 | *.pgd 88 | *.rsp 89 | *.sbr 90 | *.tlb 91 | *.tli 92 | *.tlh 93 | *.tmp 94 | *.tmp_proj 95 | *_wpftmp.csproj 96 | *.log 97 | *.vspscc 98 | *.vssscc 99 | .builds 100 | *.pidb 101 | *.svclog 102 | *.scc 103 | 104 | # Chutzpah Test files 105 | _Chutzpah* 106 | 107 | # Visual C++ cache files 108 | ipch/ 109 | *.aps 110 | *.ncb 111 | *.opendb 112 | *.opensdf 113 | *.sdf 114 | *.cachefile 115 | *.VC.db 116 | *.VC.VC.opendb 117 | 118 | # Visual Studio profiler 119 | *.psess 120 | *.vsp 121 | *.vspx 122 | *.sap 123 | 124 | # Visual Studio Trace Files 125 | *.e2e 126 | 127 | # TFS 2012 Local Workspace 128 | $tf/ 129 | 130 | # Guidance Automation Toolkit 131 | *.gpState 132 | 133 | # ReSharper is a .NET coding add-in 134 | _ReSharper*/ 135 | *.[Rr]e[Ss]harper 136 | *.DotSettings.user 137 | 138 | # TeamCity is a build add-in 139 | _TeamCity* 140 | 141 | # DotCover is a Code Coverage Tool 142 | *.dotCover 143 | 144 | # AxoCover is a Code Coverage Tool 145 | .axoCover/* 146 | !.axoCover/settings.json 147 | 148 | # Coverlet is a free, cross platform Code Coverage Tool 149 | coverage.*[.json, .xml, .info] 150 | 151 | # Visual Studio code coverage results 152 | *.coverage 153 | *.coveragexml 154 | 155 | # NCrunch 156 | _NCrunch_* 157 | .*crunch*.local.xml 158 | nCrunchTemp_* 159 | 160 | # MightyMoose 161 | *.mm.* 162 | AutoTest.Net/ 163 | 164 | # Web workbench (sass) 165 | .sass-cache/ 166 | 167 | # Installshield output folder 168 | [Ee]xpress/ 169 | 170 | # DocProject is a documentation generator add-in 171 | DocProject/buildhelp/ 172 | DocProject/Help/*.HxT 173 | DocProject/Help/*.HxC 174 | DocProject/Help/*.hhc 175 | DocProject/Help/*.hhk 176 | DocProject/Help/*.hhp 177 | DocProject/Help/Html2 178 | DocProject/Help/html 179 | 180 | # Click-Once directory 181 | publish/ 182 | 183 | # Publish Web Output 184 | *.[Pp]ublish.xml 185 | *.azurePubxml 186 | # Note: Comment the next line if you want to checkin your web deploy settings, 187 | # but database connection strings (with potential passwords) will be unencrypted 188 | *.pubxml 189 | *.publishproj 190 | 191 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 192 | # checkin your Azure Web App publish settings, but sensitive information contained 193 | # in these scripts will be unencrypted 194 | PublishScripts/ 195 | 196 | # NuGet Packages 197 | *.nupkg 198 | # NuGet Symbol Packages 199 | *.snupkg 200 | # The packages folder can be ignored because of Package Restore 201 | **/[Pp]ackages/* 202 | # except build/, which is used as an MSBuild target. 203 | !**/[Pp]ackages/build/ 204 | # Uncomment if necessary however generally it will be regenerated when needed 205 | #!**/[Pp]ackages/repositories.config 206 | # NuGet v3's project.json files produces more ignorable files 207 | *.nuget.props 208 | *.nuget.targets 209 | 210 | # Microsoft Azure Build Output 211 | csx/ 212 | *.build.csdef 213 | 214 | # Microsoft Azure Emulator 215 | ecf/ 216 | rcf/ 217 | 218 | # Windows Store app package directories and files 219 | AppPackages/ 220 | BundleArtifacts/ 221 | Package.StoreAssociation.xml 222 | _pkginfo.txt 223 | *.appx 224 | *.appxbundle 225 | *.appxupload 226 | 227 | # Visual Studio cache files 228 | # files ending in .cache can be ignored 229 | *.[Cc]ache 230 | # but keep track of directories ending in .cache 231 | !?*.[Cc]ache/ 232 | 233 | # Others 234 | ClientBin/ 235 | ~$* 236 | *~ 237 | *.dbmdl 238 | *.dbproj.schemaview 239 | *.jfm 240 | *.pfx 241 | *.publishsettings 242 | orleans.codegen.cs 243 | 244 | # Including strong name files can present a security risk 245 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 246 | #*.snk 247 | 248 | # Since there are multiple workflows, uncomment next line to ignore bower_components 249 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 250 | #bower_components/ 251 | 252 | # RIA/Silverlight projects 253 | Generated_Code/ 254 | 255 | # Backup & report files from converting an old project file 256 | # to a newer Visual Studio version. Backup files are not needed, 257 | # because we have git ;-) 258 | _UpgradeReport_Files/ 259 | Backup*/ 260 | UpgradeLog*.XML 261 | UpgradeLog*.htm 262 | ServiceFabricBackup/ 263 | *.rptproj.bak 264 | 265 | # SQL Server files 266 | *.mdf 267 | *.ldf 268 | *.ndf 269 | 270 | # Business Intelligence projects 271 | *.rdl.data 272 | *.bim.layout 273 | *.bim_*.settings 274 | *.rptproj.rsuser 275 | *- [Bb]ackup.rdl 276 | *- [Bb]ackup ([0-9]).rdl 277 | *- [Bb]ackup ([0-9][0-9]).rdl 278 | 279 | # Microsoft Fakes 280 | FakesAssemblies/ 281 | 282 | # GhostDoc plugin setting file 283 | *.GhostDoc.xml 284 | 285 | # Node.js Tools for Visual Studio 286 | .ntvs_analysis.dat 287 | node_modules/ 288 | 289 | # Visual Studio 6 build log 290 | *.plg 291 | 292 | # Visual Studio 6 workspace options file 293 | *.opt 294 | 295 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 296 | *.vbw 297 | 298 | # Visual Studio LightSwitch build output 299 | **/*.HTMLClient/GeneratedArtifacts 300 | **/*.DesktopClient/GeneratedArtifacts 301 | **/*.DesktopClient/ModelManifest.xml 302 | **/*.Server/GeneratedArtifacts 303 | **/*.Server/ModelManifest.xml 304 | _Pvt_Extensions 305 | 306 | # Paket dependency manager 307 | .paket/paket.exe 308 | paket-files/ 309 | 310 | # FAKE - F# Make 311 | .fake/ 312 | 313 | # CodeRush personal settings 314 | .cr/personal 315 | 316 | # Python Tools for Visual Studio (PTVS) 317 | __pycache__/ 318 | *.pyc 319 | 320 | # Cake - Uncomment if you are using it 321 | # tools/** 322 | # !tools/packages.config 323 | 324 | # Tabs Studio 325 | *.tss 326 | 327 | # Telerik's JustMock configuration file 328 | *.jmconfig 329 | 330 | # BizTalk build output 331 | *.btp.cs 332 | *.btm.cs 333 | *.odx.cs 334 | *.xsd.cs 335 | 336 | # OpenCover UI analysis results 337 | OpenCover/ 338 | 339 | # Azure Stream Analytics local run output 340 | ASALocalRun/ 341 | 342 | # MSBuild Binary and Structured Log 343 | *.binlog 344 | 345 | # NVidia Nsight GPU debugger configuration file 346 | *.nvuser 347 | 348 | # MFractors (Xamarin productivity tool) working folder 349 | .mfractor/ 350 | 351 | # Local History for Visual Studio 352 | .localhistory/ 353 | 354 | # BeatPulse healthcheck temp database 355 | healthchecksdb 356 | 357 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 358 | MigrationBackup/ 359 | 360 | # Ionide (cross platform F# VS Code tools) working folder 361 | .ionide/ 362 | 363 | # Rust 364 | /lib/blake3_dotnet/target 365 | Cargo.lock 366 | 367 | # Tmp folders 368 | tmp/ 369 | [Tt]emp/ 370 | 371 | # Remove artifacts produced by dotnet-releaser 372 | artifacts-dotnet-releaser/ 373 | -------------------------------------------------------------------------------- /img/FixedStrings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoofx/FixedStrings/6c7ba352c3fdbdc900b07a2088940b023cf04f08/img/FixedStrings.png -------------------------------------------------------------------------------- /img/FixedStrings.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 23 | 25 | image/svg+xml 26 | 28 | 29 | 30 | 31 | 32 | 34 | 54 | 59 | 64 | 69 | 74 | 79 | 84 | 89 | 94 | 95 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023, Alexandre Mutel 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification 5 | , are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # FixedStrings [![ci](https://github.com/xoofx/FixedStrings/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/xoofx/FixedStrings/actions/workflows/ci.yml) [![NuGet](https://img.shields.io/nuget/v/FixedStrings.svg)](https://www.nuget.org/packages/FixedStrings/) 2 | 3 | 4 | 5 | FixedStrings provides a zero allocation valuetype based fixed string implementation with the following size 8/16/32/64. 6 | 7 | ```c# 8 | // Zero allocation! 9 | FixedString16 str = $"HelloWorld {DateTime.Now.Year}"; 10 | // Prints "HelloWorld 2023" 11 | Console.Out.WriteLine(str.AsSpan()); 12 | ``` 13 | 14 | ## Features 15 | 16 | - Zero allocation via `FixedString8`, `FixedString16`, `FixedString32` and `FixedString64` structs. 17 | - Compatible with `net7.0`+ 18 | 19 | ## User Guide 20 | 21 | ### Why FixedStrings? 22 | 23 | Many modern applications are suffering from lots of allocations on the heap, and it is very frequent to see dozen of thousands of strings allocated on the managed heap. Having all these managed objects around is creating a lot more pressure on the GC, they can be more scattered in memory. 24 | 25 | In many scenarios, you might be able to co-locate such strings closer to the class that are using them, but you might be able also to refine the memory requirement/usage for these strings. 26 | 27 | For instance, on a 64 bit system, **a single allocation of an empty managed string on the heap will take 24 bytes + 8 bytes for the reference to it. That's 32 bytes for an empty string!** See [details on sharplab.io](https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEUCuA7AHwAEAmABgFgAoUgRmoEk8BnABxjAwDoAJGAQ1YAKAEQiAlAG5GLdp14DhInhOlUmbDtz6DRfADb6IAdQiqgA=). 28 | 29 | With a fixed string, like `FixedString8`, you get 7 characters and it takes only 16 bytes or a `FixedString16` would give similarly 15 characters while taking only 32 bytes! 30 | 31 | As you can see in the benchmarks below, by avoiding an allocation to the heap, **FixedStrings can be 2x as fast as creating them on the heap**. 32 | 33 | ### How does it work? 34 | 35 | FixedStrings is relying on the C# 10 [interpolated string handlers](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/interpolated-string-handler) feature. 36 | 37 | But unlike classical usage of interpolated string handlers, a FixedString is in fact itself **an interpolation handler**, using a fixed size buffer to store the string. 38 | 39 | | Fixed String Type | Maximum Number of Chars | Total Size 40 | |-------------------|-------------------------|--------------------- 41 | | `FixedString8` | 7 | 16 bytes 42 | | `FixedString16` | 15 | 32 bytes 43 | | `FixedString32` | 31 | 64 bytes 44 | | `FixedString64` | 63 | 128 bytes 45 | 46 | Each fixed string contains a `short` value for the length. 47 | 48 | For example `FixedString8` is declared like this: 49 | 50 | ```c# 51 | [InterpolatedStringHandler] 52 | public struct FixedString8 : IFixedString 53 | { 54 | private short _length; 55 | private short _c0; 56 | private short _c1; 57 | private short _c2; 58 | private short _c3; 59 | private short _c4; 60 | private short _c5; 61 | private short _c6; 62 | 63 | // ... 64 | } 65 | ``` 66 | 67 | It is using a `short` for the internal representation of each characters to avoid marshalling issues - so that each character is passed as a plain `short` (UTF-16) value to the native side. 68 | 69 | You can then copy around by value this string very easily and you can have a better control of the layout in memory. 70 | 71 | ### Usage and restrictions 72 | 73 | FixedStrings are not meant to replace all strings in your application. They are meant to be used for strings that are known to be short and that are used in a very hot path of your application. 74 | 75 | - If you try to add more characters than the size of the fixed string, **it will be truncated**. For practical reasons, It won't throw an exception! 76 | - FixedStrings support interpolation: 77 | ```c# 78 | FixedString8 str = $"Hello {name}"; 79 | ``` 80 | For performance reasons, supported types for values are: `string` and any types implementing `ISpanFormattable` (e.g `int`, `float`...etc.). 81 | A fixed string is itself `ISpanFormattable` and can be used as a value - and nested in string interpolations. 82 | 83 | > Note that if a non string interpolated value cannot fit within the fixed string, it won't be emitted and the string will be truncated (in case of using a string). For example 84 | > ```c# 85 | > FixedString8 str = $"{"Hello"}{"World"}"; 86 | > ``` 87 | > will result in ` str` containing `"HelloWo"` (7 characters). 88 | > but the following interpolation: 89 | > ```c# 90 | > FixedString8 str = $"Hello{int.MaxValue}"; 91 | > ``` 92 | > will result in ` str` containing `"Hello"`, with the int value being discarded as it cannot fit within the fixed string. 93 | - Supporting alignment and format for interpolated values 94 | ```c# 95 | byte value = 10; 96 | FixedString16 str = $"Test 0x{value:X2}"; 97 | ``` 98 | - FixedStrings support assigning from a regular string: 99 | ```c# 100 | FixedString8 str = "Hello"; 101 | ``` 102 | - You can get a `ReadOnlySpan` directly from a FixedString: 103 | ```c# 104 | FixedString8 str = "Hello"; 105 | ReadOnlySpan span = str.AsSpan(); 106 | ``` 107 | - FixedStrings can be copied by value: 108 | ```c# 109 | FixedString8 str = "Hello"; 110 | FixedString8 str2 = str; 111 | ``` 112 | `str2` will be a copy of `str`. 113 | 114 | - FixedStrings implement [`IFixedString`](https://github.com/xoofx/FixedStrings/blob/main/src/FixedStrings/IFixedString.cs) and can be used as a generic constraint: 115 | ```c# 116 | public int Foo(T value) where T : IFixedString 117 | { 118 | // ... 119 | return value.Length; 120 | } 121 | ``` 122 | 123 | ## Benchmarks 124 | 125 | `TestFixed` is using a `FixedString` and `TestDynamic` is using a regular `string`. 126 | 127 | - For `FixedString8`, the performance gain is 2.5x faster than using a regular string. 128 | - For `FixedString16`, the performance gain is 2x faster than using a regular string. 129 | - For `FixedString32` / `FixedString64`, the performance gain is 30% faster than using a regular string. 130 | 131 | | Method | Runtime | Mean | Allocated | 132 | |-------------- |--------- |---------:|----------:| 133 | | TestFixed8 | .NET 7.0 | 13.70 ns | - | 134 | | TestDynamic8 | .NET 7.0 | 31.33 ns | 40 B | 135 | | | | | | 136 | | TestFixed8 | .NET 8.0 | 10.05 ns | - | 137 | | TestDynamic8 | .NET 8.0 | 26.47 ns | 40 B | 138 | | | | | | 139 | | TestFixed16 | .NET 7.0 | 17.56 ns | - | 140 | | TestDynamic16 | .NET 7.0 | 32.52 ns | 56 B | 141 | | | | | | 142 | | TestFixed16 | .NET 8.0 | 14.25 ns | - | 143 | | TestDynamic16 | .NET 8.0 | 27.80 ns | 56 B | 144 | | | | | | 145 | | TestFixed32 | .NET 7.0 | 38.27 ns | - | 146 | | TestDynamic32 | .NET 7.0 | 49.70 ns | 86 B | 147 | | | | | | 148 | | TestFixed32 | .NET 8.0 | 30.12 ns | - | 149 | | TestDynamic32 | .NET 8.0 | 38.15 ns | 86 B | 150 | | | | | | 151 | | TestFixed64 | .NET 7.0 | 61.37 ns | - | 152 | | TestDynamic64 | .NET 7.0 | 80.15 ns | 144 B | 153 | | | | | | 154 | | TestFixed64 | .NET 8.0 | 43.96 ns | - | 155 | | TestDynamic64 | .NET 8.0 | 59.32 ns | 144 B | 156 | 157 | ## Building FixedStrings 158 | 159 | In order to build FixedStrings, you need: 160 | - Install the [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) 161 | - Install the [.NET 7 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) (For the benchmarks) 162 | 163 | ``` 164 | dotnet build -c Release src/FixedStrings.sln 165 | ``` 166 | 167 | ## License 168 | 169 | This software is released under the [BSD-2-Clause license](https://opensource.org/licenses/BSD-2-Clause). 170 | 171 | ## Author 172 | 173 | Alexandre Mutel aka [xoofx](https://xoofx.github.io). 174 | -------------------------------------------------------------------------------- /src/FixedStrings.Benchmarks/FixedStrings.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0;net8.0 6 | enable 7 | enable 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/FixedStrings.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Alexandre Mutel. All rights reserved. 2 | // Licensed under the BSD-Clause 2 license. 3 | // See license.txt file in the project root for full license information. 4 | 5 | using BenchmarkDotNet.Attributes; 6 | using BenchmarkDotNet.Running; 7 | 8 | namespace FixedStrings.Benchmarks; 9 | 10 | [MemoryDiagnoser] 11 | public class BenchString 12 | { 13 | private readonly Random _random = new Random(0); 14 | 15 | [Benchmark] 16 | public FixedString8 TestFixed8() 17 | { 18 | return $"Hello{_random.Next(100)}"; 19 | } 20 | 21 | [Benchmark] 22 | public string TestDynamic8() 23 | { 24 | return $"Hello{_random.Next(100)}"; 25 | } 26 | 27 | [Benchmark] 28 | public FixedString16 TestFixed16() 29 | { 30 | return $"Hello {_random.Next(100)} World!"; 31 | } 32 | 33 | [Benchmark] 34 | public string TestDynamic16() 35 | { 36 | return $"Hello {_random.Next(100)} World!"; 37 | } 38 | 39 | [Benchmark] 40 | public FixedString32 TestFixed32() 41 | { 42 | return $"Hello {_random.Next(100000)} World {_random.Next(100000)} Multi!"; 43 | } 44 | 45 | [Benchmark] 46 | public string TestDynamic32() 47 | { 48 | return $"Hello {_random.Next(100000)} World {_random.Next(100000)} Multi!"; 49 | } 50 | 51 | [Benchmark] 52 | public FixedString32 TestFixed64() 53 | { 54 | return $"Hello {_random.Next(100000)} World {_random.Next(100000)} Multi!Hello {_random.Next(100000)} World {_random.Next(100000)} Multi!"; 55 | } 56 | 57 | [Benchmark] 58 | public string TestDynamic64() 59 | { 60 | return $"Hello {_random.Next(100000)} World {_random.Next(100000)} Multi!Hello {_random.Next(100000)} World {_random.Next(100000)} Multi!"; 61 | } 62 | } 63 | 64 | internal class Program 65 | { 66 | static void Main(string[] args) 67 | { 68 | BenchmarkRunner.Run(null, args); 69 | } 70 | } -------------------------------------------------------------------------------- /src/FixedStrings.Tests/FixedStrings.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | false 8 | True 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/FixedStrings.Tests/TestFixedStrings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Alexandre Mutel. All rights reserved. 2 | // Licensed under the BSD-Clause 2 license. 3 | // See license.txt file in the project root for full license information. 4 | 5 | using NUnit.Framework; 6 | 7 | namespace FixedStrings.Tests; 8 | 9 | public class TestFixedStrings 10 | { 11 | [Test] 12 | public void TestHelloWorld() 13 | { 14 | FixedString16 str = $"HelloWorld {DateTime.Now.Year}"; 15 | Console.Out.WriteLine(str.AsSpan()); 16 | Assert.AreEqual($"HelloWorld {DateTime.Now.Year}", str.ToString()); 17 | } 18 | 19 | [Test] 20 | public void TestReadmeHexadecimal() 21 | { 22 | byte value = 10; 23 | FixedString16 str = $"Test 0x{value:X2}"; 24 | Assert.AreEqual($"Test 0x{value:X2}", str.ToString()); 25 | } 26 | 27 | [TestCase("")] 28 | [TestCase("0")] 29 | [TestCase("01")] 30 | [TestCase("01234567")] 31 | [TestCase("0123456789")] 32 | [TestCase("01234567890123456789")] 33 | [TestCase("012345678901234567890123456789")] 34 | [TestCase("0123456789012345678901234567890123456789")] 35 | public void Literal8(string test) 36 | { 37 | FixedString8 fs = test; 38 | AssertFixedString(fs, test); 39 | } 40 | 41 | [TestCase("")] 42 | [TestCase("0")] 43 | [TestCase("01")] 44 | [TestCase("01234567")] 45 | [TestCase("0123456789")] 46 | [TestCase("01234567890123456789")] 47 | [TestCase("012345678901234567890123456789")] 48 | [TestCase("0123456789012345678901234567890123456789")] 49 | public void Literal16(string test) 50 | { 51 | FixedString16 fs = test; 52 | AssertFixedString(fs, test); 53 | } 54 | 55 | [TestCase("")] 56 | [TestCase("0")] 57 | [TestCase("01")] 58 | [TestCase("01234567")] 59 | [TestCase("0123456789")] 60 | [TestCase("01234567890123456789")] 61 | [TestCase("012345678901234567890123456789")] 62 | [TestCase("0123456789012345678901234567890123456789")] 63 | public void Literal32(string test) 64 | { 65 | FixedString32 fs = test; 66 | AssertFixedString(fs, test); 67 | } 68 | 69 | [TestCase("")] 70 | [TestCase("0")] 71 | [TestCase("01")] 72 | [TestCase("01234567")] 73 | [TestCase("0123456789")] 74 | [TestCase("01234567890123456789")] 75 | [TestCase("012345678901234567890123456789")] 76 | [TestCase("0123456789012345678901234567890123456789")] 77 | [TestCase("012345678901234567890123456789012345678901234567890123456789")] 78 | [TestCase("0123456789012345678901234567890123456789012345678901234567890123456789")] 79 | public void Literal64(string test) 80 | { 81 | FixedString64 fs = test; 82 | AssertFixedString(fs, test); 83 | } 84 | 85 | [TestCase(0, "Hello 0")] 86 | [TestCase(12, "Hello ")] 87 | public void Interpolated8(int value, string expected) 88 | { 89 | FixedString8 fs = $"Hello {value}"; 90 | AssertFixedString(fs, expected); 91 | } 92 | 93 | [TestCase(0, "Hello 0")] 94 | [TestCase(12, "Hello 12")] 95 | [TestCase(123456789, "Hello 123456789")] 96 | [TestCase(1234567890, "Hello ")] 97 | public void Interpolated16(int value, string expected) 98 | { 99 | FixedString16 fs = $"Hello {value}"; 100 | AssertFixedString(fs, expected); 101 | } 102 | 103 | [TestCase(0, "Hello1234567890=0")] 104 | [TestCase(12, "Hello1234567890=12")] 105 | [TestCase(1234567890, "Hello1234567890=1234567890")] 106 | public void Interpolated32(int value, string expected) 107 | { 108 | FixedString32 fs = $"Hello1234567890={value}"; 109 | AssertFixedString(fs, expected); 110 | } 111 | 112 | [TestCase(0, "Hello1234567890=0")] 113 | [TestCase(12, "Hello1234567890=12")] 114 | [TestCase(1234567890, "Hello1234567890=1234567890")] 115 | public void Interpolated64(int value, string expected) 116 | { 117 | FixedString64 fs = $"Hello1234567890={value}"; 118 | AssertFixedString(fs, expected); 119 | } 120 | 121 | [Test] 122 | public void AppendLiteralOverflow() 123 | { 124 | // Check overflow on AddLiteral 125 | { 126 | var fullString = new string('A', FixedString8.MaxLength); 127 | FixedString8 fsFixed = $"{fullString}{fullString}"; 128 | Assert.AreEqual(fullString, fsFixed.ToString()); 129 | } 130 | { 131 | var fullString = new string('A', FixedString16.MaxLength); 132 | FixedString16 fsFixed = $"{fullString}{fullString}"; 133 | Assert.AreEqual(fullString, fsFixed.ToString()); 134 | } 135 | { 136 | var fullString = new string('A', FixedString32.MaxLength); 137 | FixedString32 fsFixed = $"{fullString}{fullString}"; 138 | Assert.AreEqual(fullString, fsFixed.ToString()); 139 | } 140 | { 141 | var fullString = new string('A', FixedString64.MaxLength); 142 | FixedString64 fsFixed = $"{fullString}{fullString}"; 143 | Assert.AreEqual(fullString, fsFixed.ToString()); 144 | } 145 | } 146 | 147 | [Test] 148 | public void TestAlignmentAndFormatFixedString8() 149 | { 150 | FixedString8 test; 151 | const int maxLength = 7; 152 | const string str = "Hello"; // Length = 5 153 | 154 | { 155 | // AppendFormatted 156 | int value = 10; 157 | test = $"{value,3}"; 158 | Assert.AreEqual($"{value,3}", test.ToString()); 159 | 160 | test = $"{value,3:X}"; 161 | Assert.AreEqual($"{value,3:X}", test.ToString()); 162 | 163 | test = $"{value:X}"; 164 | Assert.AreEqual($"{value:X}", test.ToString()); 165 | } 166 | 167 | test = $"{str,-5}"; 168 | Assert.AreEqual($"{str,-5}", test.ToString()); 169 | 170 | test = $"{str,0}"; 171 | Assert.AreEqual($"{str,0}", test.ToString()); 172 | 173 | test = $"{str,-7}"; 174 | Assert.AreEqual($"{str,-7}"[..maxLength], test.ToString()); 175 | 176 | test = $"{str,-8}"; 177 | Assert.AreEqual($"{str,-8}"[..maxLength], test.ToString()); 178 | 179 | test = $"{str,7}"; 180 | Assert.AreEqual($"{str,7}"[..maxLength], test.ToString()); 181 | 182 | test = $"{str,11}"; 183 | Assert.AreEqual($"{str,11}"[..maxLength], test.ToString()); 184 | 185 | test = $"{str,12}"; 186 | Assert.AreEqual($"{str,12}"[..maxLength], test.ToString()); 187 | 188 | test = $"{str,13}"; 189 | Assert.AreEqual($"{str,13}"[..maxLength], test.ToString()); 190 | } 191 | 192 | [Test] 193 | public void TestAlignmentAndFormatFixedString16() 194 | { 195 | FixedString16 test; 196 | const int maxLength = 15; 197 | const string str = "Hello01234567"; // Length = 13 198 | 199 | { 200 | // AppendFormatted 201 | int value = 10; 202 | test = $"{value,3}"; 203 | Assert.AreEqual($"{value,3}", test.ToString()); 204 | 205 | test = $"{value,3:X}"; 206 | Assert.AreEqual($"{value,3:X}", test.ToString()); 207 | 208 | test = $"{value:X}"; 209 | Assert.AreEqual($"{value:X}", test.ToString()); 210 | } 211 | 212 | test = $"{str,-13}"; 213 | Assert.AreEqual($"{str,-13}", test.ToString()); 214 | 215 | test = $"{str,0}"; 216 | Assert.AreEqual($"{str,0}", test.ToString()); 217 | 218 | test = $"{str,-15}"; 219 | Assert.AreEqual($"{str,-15}"[..maxLength], test.ToString()); 220 | 221 | test = $"{str,-16}"; 222 | Assert.AreEqual($"{str,-16}"[..maxLength], test.ToString()); 223 | 224 | test = $"{str,15}"; 225 | Assert.AreEqual($"{str,15}"[..maxLength], test.ToString()); 226 | 227 | test = $"{str,19}"; 228 | Assert.AreEqual($"{str,19}"[..maxLength], test.ToString()); 229 | 230 | test = $"{str,20}"; 231 | Assert.AreEqual($"{str,20}"[..maxLength], test.ToString()); 232 | 233 | test = $"{str,21}"; 234 | Assert.AreEqual($"{str,21}"[..maxLength], test.ToString()); 235 | } 236 | 237 | [Test] 238 | public void TestAlignmentAndFormatFixedString32() 239 | { 240 | FixedString32 test; 241 | const int maxLength = 31; 242 | const string str = "Hello012345678901234567890123"; // Length = 29 243 | 244 | { 245 | // AppendFormatted 246 | int value = 10; 247 | test = $"{value,3}"; 248 | Assert.AreEqual($"{value,3}", test.ToString()); 249 | 250 | test = $"{value,3:X}"; 251 | Assert.AreEqual($"{value,3:X}", test.ToString()); 252 | 253 | test = $"{value:X}"; 254 | Assert.AreEqual($"{value:X}", test.ToString()); 255 | } 256 | 257 | test = $"{str,-29}"; 258 | Assert.AreEqual($"{str,-29}", test.ToString()); 259 | 260 | test = $"{str,0}"; 261 | Assert.AreEqual($"{str,0}", test.ToString()); 262 | 263 | test = $"{str,-31}"; 264 | Assert.AreEqual($"{str,-31}"[..maxLength], test.ToString()); 265 | 266 | test = $"{str,-32}"; 267 | Assert.AreEqual($"{str,-32}"[..maxLength], test.ToString()); 268 | 269 | test = $"{str,31}"; 270 | Assert.AreEqual($"{str,31}"[..maxLength], test.ToString()); 271 | 272 | test = $"{str,35}"; 273 | Assert.AreEqual($"{str,35}"[..maxLength], test.ToString()); 274 | 275 | test = $"{str,36}"; 276 | Assert.AreEqual($"{str,36}"[..maxLength], test.ToString()); 277 | 278 | test = $"{str,37}"; 279 | Assert.AreEqual($"{str,37}"[..maxLength], test.ToString()); 280 | } 281 | 282 | [Test] 283 | public void TestAlignmentAndFormatFixedString64() 284 | { 285 | FixedString64 test; 286 | const int maxLength = 63; 287 | const string str = "Hello01234567890123456789012345678901234567890123456789012345"; // Length = 61 288 | 289 | { 290 | // AppendFormatted 291 | int value = 10; 292 | test = $"{value,3}"; 293 | Assert.AreEqual($"{value,3}", test.ToString()); 294 | 295 | test = $"{value,3:X}"; 296 | Assert.AreEqual($"{value,3:X}", test.ToString()); 297 | 298 | test = $"{value:X}"; 299 | Assert.AreEqual($"{value:X}", test.ToString()); 300 | } 301 | 302 | test = $"{str,-61}"; 303 | Assert.AreEqual($"{str,-61}", test.ToString()); 304 | 305 | test = $"{str,0}"; 306 | Assert.AreEqual($"{str,0}", test.ToString()); 307 | 308 | test = $"{str,-63}"; 309 | Assert.AreEqual($"{str,-63}"[..maxLength], test.ToString()); 310 | 311 | test = $"{str,-64}"; 312 | Assert.AreEqual($"{str,-64}"[..maxLength], test.ToString()); 313 | 314 | test = $"{str,63}"; 315 | Assert.AreEqual($"{str,63}"[..maxLength], test.ToString()); 316 | 317 | test = $"{str,64}"; 318 | Assert.AreEqual($"{str,64}"[..maxLength], test.ToString()); 319 | 320 | test = $"{str,65}"; 321 | Assert.AreEqual($"{str,65}"[..maxLength], test.ToString()); 322 | 323 | test = $"{str,66}"; 324 | Assert.AreEqual($"{str,66}"[..maxLength], test.ToString()); 325 | } 326 | 327 | private static unsafe void AssertFixedString(TFixedString fs, string test) where TFixedString: unmanaged, IFixedString 328 | { 329 | // Verify the size of the fixed struct 330 | Assert.AreEqual((TFixedString.MaxLength + 1) * sizeof(short), sizeof(TFixedString)); 331 | 332 | // Verify ToString(); 333 | var expectedLength = Math.Min(test.Length, TFixedString.MaxLength); 334 | var expectedString = test.Substring(0, expectedLength); 335 | Assert.AreEqual(expectedString, fs.ToString()); 336 | 337 | // Verify Length 338 | Assert.AreEqual(expectedLength, fs.Length); 339 | 340 | // Verify GetHashCode 341 | Assert.NotNull(fs.GetHashCode()); 342 | 343 | // Verify AsSpan() 344 | Assert.True(expectedString.AsSpan().SequenceEqual(fs.GetUnsafeFullSpan().Slice(0, fs.Length))); 345 | 346 | // Verify TryFormat() 347 | var tryFormat = $"{fs}"; 348 | Assert.AreEqual(expectedString, tryFormat); 349 | 350 | // Test Equals and implicit operator 351 | TFixedString fsCopy = expectedString; 352 | Assert.AreEqual(fsCopy, fs); 353 | Assert.True(fsCopy.Equals((object)fs)); 354 | Assert.False(fsCopy.Equals((object)1)); 355 | 356 | // Verify Clear() 357 | fs.Clear(); 358 | Assert.AreEqual(0, fs.Length); 359 | 360 | TFixedString tooBig = "ABCD"; 361 | Span span = stackalloc char[3]; 362 | Assert.False(tooBig.TryFormat(span, out var charsWritten, ReadOnlySpan.Empty, null)); 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /src/FixedStrings.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32014.148 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FixedStrings", "FixedStrings\FixedStrings.csproj", "{FBA4AF90-A360-4D4A-8399-6EDB491E3F43}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FixedStrings.Tests", "FixedStrings.Tests\FixedStrings.Tests.csproj", "{94ECE203-A84A-4018-872C-078E91C1AB43}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BBC4B16D-083F-47E6-BFAE-9064E600F230}" 11 | ProjectSection(SolutionItems) = preProject 12 | ..\.editorconfig = ..\.editorconfig 13 | ..\.gitattributes = ..\.gitattributes 14 | ..\.gitignore = ..\.gitignore 15 | ..\.github\workflows\ci.yml = ..\.github\workflows\ci.yml 16 | dotnet-releaser.toml = dotnet-releaser.toml 17 | global.json = global.json 18 | ..\license.txt = ..\license.txt 19 | ..\readme.md = ..\readme.md 20 | EndProjectSection 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FixedStrings.Benchmarks", "FixedStrings.Benchmarks\FixedStrings.Benchmarks.csproj", "{7B2921F2-D284-4C92-8C29-38675881C179}" 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Release|Any CPU = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {FBA4AF90-A360-4D4A-8399-6EDB491E3F43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {FBA4AF90-A360-4D4A-8399-6EDB491E3F43}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {FBA4AF90-A360-4D4A-8399-6EDB491E3F43}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {FBA4AF90-A360-4D4A-8399-6EDB491E3F43}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {94ECE203-A84A-4018-872C-078E91C1AB43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {94ECE203-A84A-4018-872C-078E91C1AB43}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {94ECE203-A84A-4018-872C-078E91C1AB43}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {94ECE203-A84A-4018-872C-078E91C1AB43}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {7B2921F2-D284-4C92-8C29-38675881C179}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {7B2921F2-D284-4C92-8C29-38675881C179}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {7B2921F2-D284-4C92-8C29-38675881C179}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {7B2921F2-D284-4C92-8C29-38675881C179}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {95E8F3B3-49E8-400E-97A1-38D8B946DFCA} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /src/FixedStrings/FixedStrings.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Library 4 | net7.0 5 | enable 6 | true 7 | 8 | 9 | 10 | FixedStrings provides a zero allocation valuetype based fixed string implementation with the following size 8/16/32/64. 11 | Alexandre Mutel 12 | en-US 13 | Alexandre Mutel 14 | fixedstring;performance 15 | readme.md 16 | FixedStrings.png 17 | https://github.com/xoofx/ 18 | BSD-2-Clause 19 | 20 | true 21 | true 22 | snupkg 23 | True 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | all 34 | runtime; build; native; contentfiles; analyzers; buildtransitive 35 | 36 | 37 | 38 | 39 | 40 | TextTemplatingFileGenerator 41 | FixedStrings.g.cs 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | True 52 | True 53 | FixedStrings.tt 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/FixedStrings/FixedStrings.g.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Alexandre Mutel. All rights reserved. 2 | // Licensed under the BSD-Clause 2 license. 3 | // See license.txt file in the project root for full license information. 4 | using System; 5 | using System.ComponentModel; 6 | using System.Diagnostics.CodeAnalysis; 7 | using System.Runtime.CompilerServices; 8 | using System.Runtime.InteropServices; 9 | 10 | #nullable enable 11 | 12 | namespace FixedStrings; 13 | 14 | /// 15 | /// Represents a fixed-length string of maximum 7 characters. 16 | /// 17 | [InterpolatedStringHandler] 18 | public struct FixedString8 : IFixedString 19 | { 20 | /// 21 | public static int MaxLength 22 | { 23 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 24 | get => 7; 25 | } 26 | 27 | private short _length; 28 | #pragma warning disable CS0169 // Field is never used 29 | // Use short instead of char to make this struct blittable 30 | private short _c0; 31 | private short _c1; 32 | private short _c2; 33 | private short _c3; 34 | private short _c4; 35 | private short _c5; 36 | private short _c6; 37 | #pragma warning restore CS0169 // Field is never used 38 | 39 | /// 40 | /// Initializes a new instance of the struct. 41 | /// 42 | public FixedString8(int literalLength, int formattedCount) 43 | { 44 | _length = 0; 45 | } 46 | 47 | /// 48 | /// Initializes a new instance of the struct. 49 | /// 50 | public FixedString8(string value) 51 | { 52 | _length = 0; 53 | AppendLiteral(value); 54 | } 55 | 56 | /// 57 | public readonly int Length 58 | { 59 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 60 | get => _length; 61 | } 62 | 63 | /// 64 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 65 | public void Clear() => _length = 0; 66 | 67 | /// 68 | /// Appends the string literal to this fixed string. 69 | /// 70 | [EditorBrowsable(EditorBrowsableState.Never)] 71 | public void AppendLiteral(ReadOnlySpan s) 72 | { 73 | if (_length == MaxLength) return; 74 | 75 | var span = AsRemainingSpan(); 76 | if (span.Length < s.Length) 77 | { 78 | s.Slice(0, span.Length).CopyTo(span); 79 | _length += (short)span.Length; 80 | } 81 | else 82 | { 83 | s.CopyTo(span); 84 | _length += (short)s.Length; 85 | } 86 | } 87 | 88 | /// 89 | /// Apppends the specified string to this fixed string. 90 | /// 91 | [EditorBrowsable(EditorBrowsableState.Never)] 92 | public void AppendFormatted(string t) => AppendLiteral(t); 93 | 94 | /// 95 | /// Apppends the specified string to this fixed string. 96 | /// 97 | [EditorBrowsable(EditorBrowsableState.Never)] 98 | public void AppendFormatted(string t, int alignment) 99 | { 100 | var startPosition = _length; 101 | AppendFormatted(t); 102 | AppendOrInsertAlignment(startPosition, alignment); 103 | } 104 | 105 | /// 106 | /// Apppends the formatted value to this fixed string. 107 | /// 108 | [EditorBrowsable(EditorBrowsableState.Never)] 109 | public void AppendFormatted(T t) where T : ISpanFormattable 110 | { 111 | var span = AsRemainingSpan(); 112 | t.TryFormat(span, out int charsWritten, new ReadOnlySpan(), null); 113 | _length += (short)charsWritten; 114 | } 115 | 116 | /// 117 | /// Apppends the formatted value to this fixed string. 118 | /// 119 | [EditorBrowsable(EditorBrowsableState.Never)] 120 | public void AppendFormatted(T value, string? format) where T : ISpanFormattable 121 | { 122 | var span = AsRemainingSpan(); 123 | value.TryFormat(span, out int charsWritten, format, null); 124 | _length += (short)charsWritten; 125 | } 126 | 127 | /// 128 | /// Apppends the formatted value to this fixed string. 129 | /// 130 | [EditorBrowsable(EditorBrowsableState.Never)] 131 | public void AppendFormatted(T value, int alignment) where T : ISpanFormattable 132 | { 133 | var startPosition = _length; 134 | AppendFormatted(value); 135 | AppendOrInsertAlignment(startPosition, alignment); 136 | } 137 | 138 | /// 139 | /// Apppends the formatted value to this fixed string. 140 | /// 141 | [EditorBrowsable(EditorBrowsableState.Never)] 142 | public void AppendFormatted(T value, int alignment, string? format) where T : ISpanFormattable 143 | { 144 | var startPosition = _length; 145 | AppendFormatted(value, format); 146 | AppendOrInsertAlignment(startPosition, alignment); 147 | } 148 | 149 | /// } 150 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 151 | public bool Equals(FixedString8 other) => _length == other._length && AsSpan().SequenceEqual(other.AsSpan()); 152 | 153 | /// 154 | public override bool Equals(object? obj) => obj is FixedString8 other && Equals(other); 155 | 156 | /// 157 | public override int GetHashCode() 158 | { 159 | // Compute the FNV-1a hash of the string 160 | int hash = unchecked((int)2166136261); 161 | foreach (var c in AsSpan()) 162 | { 163 | hash ^= c; 164 | hash *= 16777619; 165 | } 166 | 167 | return hash; 168 | } 169 | 170 | /// 171 | /// Returns a span of characters that contains the characters of this string. 172 | /// 173 | /// A span of characters that contains the characters of this string. 174 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 175 | [UnscopedRef] 176 | public readonly ReadOnlySpan AsSpan() => MemoryMarshal.CreateSpan(ref Unsafe.As(ref Unsafe.AsRef(_c0)), _length); 177 | 178 | Span IFixedString.GetUnsafeFullSpan() => AsUnsafeFullSpan(); 179 | 180 | /// 181 | /// Implicit conversion from to . 182 | /// 183 | [SkipLocalsInit] 184 | public static implicit operator FixedString8(string s) => new(s); 185 | 186 | /// 187 | public readonly override string ToString() => ToString(null, null); 188 | 189 | /// 190 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 191 | public readonly string ToString(string? format, IFormatProvider? formatProvider) => _length == 0 ? string.Empty : new(AsSpan()); 192 | 193 | /// 194 | [EditorBrowsable(EditorBrowsableState.Never)] 195 | public readonly bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) 196 | { 197 | if (destination.Length < _length) 198 | { 199 | charsWritten = 0; 200 | return false; 201 | } 202 | AsSpan().CopyTo(destination); 203 | charsWritten = _length; 204 | return true; 205 | } 206 | 207 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 208 | private Span AsUnsafeFullSpan() => MemoryMarshal.CreateSpan(ref Unsafe.As(ref _c0), MaxLength); 209 | 210 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 211 | [UnscopedRef] 212 | private Span AsRemainingSpan() => AsUnsafeFullSpan().Slice(_length); 213 | 214 | /// 215 | /// Appends or inserts the specified alignment at the specified position. 216 | /// 217 | private void AppendOrInsertAlignment(int startPosition, int alignment) 218 | { 219 | if (alignment == 0) return; 220 | 221 | int length = _length - startPosition; 222 | bool padAfter = false; 223 | if (alignment < 0) 224 | { 225 | padAfter = true; 226 | alignment = -alignment; 227 | } 228 | 229 | int numberOfCharsToAppendOrInsert = alignment - length; 230 | if (numberOfCharsToAppendOrInsert <= 0) return; 231 | 232 | if (padAfter) 233 | { 234 | numberOfCharsToAppendOrInsert = Math.Min(MaxLength, _length + numberOfCharsToAppendOrInsert) - _length; 235 | 236 | if (numberOfCharsToAppendOrInsert > 0) 237 | { 238 | AsRemainingSpan().Slice(0, numberOfCharsToAppendOrInsert).Fill(' '); 239 | _length += (short)numberOfCharsToAppendOrInsert; 240 | } 241 | } 242 | else 243 | { 244 | numberOfCharsToAppendOrInsert = Math.Min(MaxLength, startPosition + numberOfCharsToAppendOrInsert) - startPosition; 245 | 246 | if (numberOfCharsToAppendOrInsert > 0) 247 | { 248 | var endPositionFill = startPosition + numberOfCharsToAppendOrInsert; 249 | 250 | var span = AsUnsafeFullSpan(); 251 | if (endPositionFill < MaxLength) 252 | { 253 | var maxLengthToCopy = Math.Min(MaxLength, endPositionFill + length) - endPositionFill; 254 | if (maxLengthToCopy > 0) 255 | { 256 | span.Slice(startPosition, maxLengthToCopy).CopyTo(span.Slice(endPositionFill, maxLengthToCopy)); 257 | } 258 | } 259 | 260 | span.Slice(startPosition, numberOfCharsToAppendOrInsert).Fill(' '); 261 | _length = (short)Math.Min(endPositionFill + length, MaxLength); 262 | } 263 | } 264 | } 265 | } 266 | 267 | /// 268 | /// Represents a fixed-length string of maximum 15 characters. 269 | /// 270 | [InterpolatedStringHandler] 271 | public struct FixedString16 : IFixedString 272 | { 273 | /// 274 | public static int MaxLength 275 | { 276 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 277 | get => 15; 278 | } 279 | 280 | private short _length; 281 | #pragma warning disable CS0169 // Field is never used 282 | // Use short instead of char to make this struct blittable 283 | private short _c0; 284 | private short _c1; 285 | private short _c2; 286 | private short _c3; 287 | private short _c4; 288 | private short _c5; 289 | private short _c6; 290 | private short _c7; 291 | private short _c8; 292 | private short _c9; 293 | private short _c10; 294 | private short _c11; 295 | private short _c12; 296 | private short _c13; 297 | private short _c14; 298 | #pragma warning restore CS0169 // Field is never used 299 | 300 | /// 301 | /// Initializes a new instance of the struct. 302 | /// 303 | public FixedString16(int literalLength, int formattedCount) 304 | { 305 | _length = 0; 306 | } 307 | 308 | /// 309 | /// Initializes a new instance of the struct. 310 | /// 311 | public FixedString16(string value) 312 | { 313 | _length = 0; 314 | AppendLiteral(value); 315 | } 316 | 317 | /// 318 | public readonly int Length 319 | { 320 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 321 | get => _length; 322 | } 323 | 324 | /// 325 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 326 | public void Clear() => _length = 0; 327 | 328 | /// 329 | /// Appends the string literal to this fixed string. 330 | /// 331 | [EditorBrowsable(EditorBrowsableState.Never)] 332 | public void AppendLiteral(ReadOnlySpan s) 333 | { 334 | if (_length == MaxLength) return; 335 | 336 | var span = AsRemainingSpan(); 337 | if (span.Length < s.Length) 338 | { 339 | s.Slice(0, span.Length).CopyTo(span); 340 | _length += (short)span.Length; 341 | } 342 | else 343 | { 344 | s.CopyTo(span); 345 | _length += (short)s.Length; 346 | } 347 | } 348 | 349 | /// 350 | /// Apppends the specified string to this fixed string. 351 | /// 352 | [EditorBrowsable(EditorBrowsableState.Never)] 353 | public void AppendFormatted(string t) => AppendLiteral(t); 354 | 355 | /// 356 | /// Apppends the specified string to this fixed string. 357 | /// 358 | [EditorBrowsable(EditorBrowsableState.Never)] 359 | public void AppendFormatted(string t, int alignment) 360 | { 361 | var startPosition = _length; 362 | AppendFormatted(t); 363 | AppendOrInsertAlignment(startPosition, alignment); 364 | } 365 | 366 | /// 367 | /// Apppends the formatted value to this fixed string. 368 | /// 369 | [EditorBrowsable(EditorBrowsableState.Never)] 370 | public void AppendFormatted(T t) where T : ISpanFormattable 371 | { 372 | var span = AsRemainingSpan(); 373 | t.TryFormat(span, out int charsWritten, new ReadOnlySpan(), null); 374 | _length += (short)charsWritten; 375 | } 376 | 377 | /// 378 | /// Apppends the formatted value to this fixed string. 379 | /// 380 | [EditorBrowsable(EditorBrowsableState.Never)] 381 | public void AppendFormatted(T value, string? format) where T : ISpanFormattable 382 | { 383 | var span = AsRemainingSpan(); 384 | value.TryFormat(span, out int charsWritten, format, null); 385 | _length += (short)charsWritten; 386 | } 387 | 388 | /// 389 | /// Apppends the formatted value to this fixed string. 390 | /// 391 | [EditorBrowsable(EditorBrowsableState.Never)] 392 | public void AppendFormatted(T value, int alignment) where T : ISpanFormattable 393 | { 394 | var startPosition = _length; 395 | AppendFormatted(value); 396 | AppendOrInsertAlignment(startPosition, alignment); 397 | } 398 | 399 | /// 400 | /// Apppends the formatted value to this fixed string. 401 | /// 402 | [EditorBrowsable(EditorBrowsableState.Never)] 403 | public void AppendFormatted(T value, int alignment, string? format) where T : ISpanFormattable 404 | { 405 | var startPosition = _length; 406 | AppendFormatted(value, format); 407 | AppendOrInsertAlignment(startPosition, alignment); 408 | } 409 | 410 | /// } 411 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 412 | public bool Equals(FixedString16 other) => _length == other._length && AsSpan().SequenceEqual(other.AsSpan()); 413 | 414 | /// 415 | public override bool Equals(object? obj) => obj is FixedString16 other && Equals(other); 416 | 417 | /// 418 | public override int GetHashCode() 419 | { 420 | // Compute the FNV-1a hash of the string 421 | int hash = unchecked((int)2166136261); 422 | foreach (var c in AsSpan()) 423 | { 424 | hash ^= c; 425 | hash *= 16777619; 426 | } 427 | 428 | return hash; 429 | } 430 | 431 | /// 432 | /// Returns a span of characters that contains the characters of this string. 433 | /// 434 | /// A span of characters that contains the characters of this string. 435 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 436 | [UnscopedRef] 437 | public readonly ReadOnlySpan AsSpan() => MemoryMarshal.CreateSpan(ref Unsafe.As(ref Unsafe.AsRef(_c0)), _length); 438 | 439 | Span IFixedString.GetUnsafeFullSpan() => AsUnsafeFullSpan(); 440 | 441 | /// 442 | /// Implicit conversion from to . 443 | /// 444 | [SkipLocalsInit] 445 | public static implicit operator FixedString16(string s) => new(s); 446 | 447 | /// 448 | public readonly override string ToString() => ToString(null, null); 449 | 450 | /// 451 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 452 | public readonly string ToString(string? format, IFormatProvider? formatProvider) => _length == 0 ? string.Empty : new(AsSpan()); 453 | 454 | /// 455 | [EditorBrowsable(EditorBrowsableState.Never)] 456 | public readonly bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) 457 | { 458 | if (destination.Length < _length) 459 | { 460 | charsWritten = 0; 461 | return false; 462 | } 463 | AsSpan().CopyTo(destination); 464 | charsWritten = _length; 465 | return true; 466 | } 467 | 468 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 469 | private Span AsUnsafeFullSpan() => MemoryMarshal.CreateSpan(ref Unsafe.As(ref _c0), MaxLength); 470 | 471 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 472 | [UnscopedRef] 473 | private Span AsRemainingSpan() => AsUnsafeFullSpan().Slice(_length); 474 | 475 | /// 476 | /// Appends or inserts the specified alignment at the specified position. 477 | /// 478 | private void AppendOrInsertAlignment(int startPosition, int alignment) 479 | { 480 | if (alignment == 0) return; 481 | 482 | int length = _length - startPosition; 483 | bool padAfter = false; 484 | if (alignment < 0) 485 | { 486 | padAfter = true; 487 | alignment = -alignment; 488 | } 489 | 490 | int numberOfCharsToAppendOrInsert = alignment - length; 491 | if (numberOfCharsToAppendOrInsert <= 0) return; 492 | 493 | if (padAfter) 494 | { 495 | numberOfCharsToAppendOrInsert = Math.Min(MaxLength, _length + numberOfCharsToAppendOrInsert) - _length; 496 | 497 | if (numberOfCharsToAppendOrInsert > 0) 498 | { 499 | AsRemainingSpan().Slice(0, numberOfCharsToAppendOrInsert).Fill(' '); 500 | _length += (short)numberOfCharsToAppendOrInsert; 501 | } 502 | } 503 | else 504 | { 505 | numberOfCharsToAppendOrInsert = Math.Min(MaxLength, startPosition + numberOfCharsToAppendOrInsert) - startPosition; 506 | 507 | if (numberOfCharsToAppendOrInsert > 0) 508 | { 509 | var endPositionFill = startPosition + numberOfCharsToAppendOrInsert; 510 | 511 | var span = AsUnsafeFullSpan(); 512 | if (endPositionFill < MaxLength) 513 | { 514 | var maxLengthToCopy = Math.Min(MaxLength, endPositionFill + length) - endPositionFill; 515 | if (maxLengthToCopy > 0) 516 | { 517 | span.Slice(startPosition, maxLengthToCopy).CopyTo(span.Slice(endPositionFill, maxLengthToCopy)); 518 | } 519 | } 520 | 521 | span.Slice(startPosition, numberOfCharsToAppendOrInsert).Fill(' '); 522 | _length = (short)Math.Min(endPositionFill + length, MaxLength); 523 | } 524 | } 525 | } 526 | } 527 | 528 | /// 529 | /// Represents a fixed-length string of maximum 31 characters. 530 | /// 531 | [InterpolatedStringHandler] 532 | public struct FixedString32 : IFixedString 533 | { 534 | /// 535 | public static int MaxLength 536 | { 537 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 538 | get => 31; 539 | } 540 | 541 | private short _length; 542 | #pragma warning disable CS0169 // Field is never used 543 | // Use short instead of char to make this struct blittable 544 | private short _c0; 545 | private short _c1; 546 | private short _c2; 547 | private short _c3; 548 | private short _c4; 549 | private short _c5; 550 | private short _c6; 551 | private short _c7; 552 | private short _c8; 553 | private short _c9; 554 | private short _c10; 555 | private short _c11; 556 | private short _c12; 557 | private short _c13; 558 | private short _c14; 559 | private short _c15; 560 | private short _c16; 561 | private short _c17; 562 | private short _c18; 563 | private short _c19; 564 | private short _c20; 565 | private short _c21; 566 | private short _c22; 567 | private short _c23; 568 | private short _c24; 569 | private short _c25; 570 | private short _c26; 571 | private short _c27; 572 | private short _c28; 573 | private short _c29; 574 | private short _c30; 575 | #pragma warning restore CS0169 // Field is never used 576 | 577 | /// 578 | /// Initializes a new instance of the struct. 579 | /// 580 | public FixedString32(int literalLength, int formattedCount) 581 | { 582 | _length = 0; 583 | } 584 | 585 | /// 586 | /// Initializes a new instance of the struct. 587 | /// 588 | public FixedString32(string value) 589 | { 590 | _length = 0; 591 | AppendLiteral(value); 592 | } 593 | 594 | /// 595 | public readonly int Length 596 | { 597 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 598 | get => _length; 599 | } 600 | 601 | /// 602 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 603 | public void Clear() => _length = 0; 604 | 605 | /// 606 | /// Appends the string literal to this fixed string. 607 | /// 608 | [EditorBrowsable(EditorBrowsableState.Never)] 609 | public void AppendLiteral(ReadOnlySpan s) 610 | { 611 | if (_length == MaxLength) return; 612 | 613 | var span = AsRemainingSpan(); 614 | if (span.Length < s.Length) 615 | { 616 | s.Slice(0, span.Length).CopyTo(span); 617 | _length += (short)span.Length; 618 | } 619 | else 620 | { 621 | s.CopyTo(span); 622 | _length += (short)s.Length; 623 | } 624 | } 625 | 626 | /// 627 | /// Apppends the specified string to this fixed string. 628 | /// 629 | [EditorBrowsable(EditorBrowsableState.Never)] 630 | public void AppendFormatted(string t) => AppendLiteral(t); 631 | 632 | /// 633 | /// Apppends the specified string to this fixed string. 634 | /// 635 | [EditorBrowsable(EditorBrowsableState.Never)] 636 | public void AppendFormatted(string t, int alignment) 637 | { 638 | var startPosition = _length; 639 | AppendFormatted(t); 640 | AppendOrInsertAlignment(startPosition, alignment); 641 | } 642 | 643 | /// 644 | /// Apppends the formatted value to this fixed string. 645 | /// 646 | [EditorBrowsable(EditorBrowsableState.Never)] 647 | public void AppendFormatted(T t) where T : ISpanFormattable 648 | { 649 | var span = AsRemainingSpan(); 650 | t.TryFormat(span, out int charsWritten, new ReadOnlySpan(), null); 651 | _length += (short)charsWritten; 652 | } 653 | 654 | /// 655 | /// Apppends the formatted value to this fixed string. 656 | /// 657 | [EditorBrowsable(EditorBrowsableState.Never)] 658 | public void AppendFormatted(T value, string? format) where T : ISpanFormattable 659 | { 660 | var span = AsRemainingSpan(); 661 | value.TryFormat(span, out int charsWritten, format, null); 662 | _length += (short)charsWritten; 663 | } 664 | 665 | /// 666 | /// Apppends the formatted value to this fixed string. 667 | /// 668 | [EditorBrowsable(EditorBrowsableState.Never)] 669 | public void AppendFormatted(T value, int alignment) where T : ISpanFormattable 670 | { 671 | var startPosition = _length; 672 | AppendFormatted(value); 673 | AppendOrInsertAlignment(startPosition, alignment); 674 | } 675 | 676 | /// 677 | /// Apppends the formatted value to this fixed string. 678 | /// 679 | [EditorBrowsable(EditorBrowsableState.Never)] 680 | public void AppendFormatted(T value, int alignment, string? format) where T : ISpanFormattable 681 | { 682 | var startPosition = _length; 683 | AppendFormatted(value, format); 684 | AppendOrInsertAlignment(startPosition, alignment); 685 | } 686 | 687 | /// } 688 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 689 | public bool Equals(FixedString32 other) => _length == other._length && AsSpan().SequenceEqual(other.AsSpan()); 690 | 691 | /// 692 | public override bool Equals(object? obj) => obj is FixedString32 other && Equals(other); 693 | 694 | /// 695 | public override int GetHashCode() 696 | { 697 | // Compute the FNV-1a hash of the string 698 | int hash = unchecked((int)2166136261); 699 | foreach (var c in AsSpan()) 700 | { 701 | hash ^= c; 702 | hash *= 16777619; 703 | } 704 | 705 | return hash; 706 | } 707 | 708 | /// 709 | /// Returns a span of characters that contains the characters of this string. 710 | /// 711 | /// A span of characters that contains the characters of this string. 712 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 713 | [UnscopedRef] 714 | public readonly ReadOnlySpan AsSpan() => MemoryMarshal.CreateSpan(ref Unsafe.As(ref Unsafe.AsRef(_c0)), _length); 715 | 716 | Span IFixedString.GetUnsafeFullSpan() => AsUnsafeFullSpan(); 717 | 718 | /// 719 | /// Implicit conversion from to . 720 | /// 721 | [SkipLocalsInit] 722 | public static implicit operator FixedString32(string s) => new(s); 723 | 724 | /// 725 | public readonly override string ToString() => ToString(null, null); 726 | 727 | /// 728 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 729 | public readonly string ToString(string? format, IFormatProvider? formatProvider) => _length == 0 ? string.Empty : new(AsSpan()); 730 | 731 | /// 732 | [EditorBrowsable(EditorBrowsableState.Never)] 733 | public readonly bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) 734 | { 735 | if (destination.Length < _length) 736 | { 737 | charsWritten = 0; 738 | return false; 739 | } 740 | AsSpan().CopyTo(destination); 741 | charsWritten = _length; 742 | return true; 743 | } 744 | 745 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 746 | private Span AsUnsafeFullSpan() => MemoryMarshal.CreateSpan(ref Unsafe.As(ref _c0), MaxLength); 747 | 748 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 749 | [UnscopedRef] 750 | private Span AsRemainingSpan() => AsUnsafeFullSpan().Slice(_length); 751 | 752 | /// 753 | /// Appends or inserts the specified alignment at the specified position. 754 | /// 755 | private void AppendOrInsertAlignment(int startPosition, int alignment) 756 | { 757 | if (alignment == 0) return; 758 | 759 | int length = _length - startPosition; 760 | bool padAfter = false; 761 | if (alignment < 0) 762 | { 763 | padAfter = true; 764 | alignment = -alignment; 765 | } 766 | 767 | int numberOfCharsToAppendOrInsert = alignment - length; 768 | if (numberOfCharsToAppendOrInsert <= 0) return; 769 | 770 | if (padAfter) 771 | { 772 | numberOfCharsToAppendOrInsert = Math.Min(MaxLength, _length + numberOfCharsToAppendOrInsert) - _length; 773 | 774 | if (numberOfCharsToAppendOrInsert > 0) 775 | { 776 | AsRemainingSpan().Slice(0, numberOfCharsToAppendOrInsert).Fill(' '); 777 | _length += (short)numberOfCharsToAppendOrInsert; 778 | } 779 | } 780 | else 781 | { 782 | numberOfCharsToAppendOrInsert = Math.Min(MaxLength, startPosition + numberOfCharsToAppendOrInsert) - startPosition; 783 | 784 | if (numberOfCharsToAppendOrInsert > 0) 785 | { 786 | var endPositionFill = startPosition + numberOfCharsToAppendOrInsert; 787 | 788 | var span = AsUnsafeFullSpan(); 789 | if (endPositionFill < MaxLength) 790 | { 791 | var maxLengthToCopy = Math.Min(MaxLength, endPositionFill + length) - endPositionFill; 792 | if (maxLengthToCopy > 0) 793 | { 794 | span.Slice(startPosition, maxLengthToCopy).CopyTo(span.Slice(endPositionFill, maxLengthToCopy)); 795 | } 796 | } 797 | 798 | span.Slice(startPosition, numberOfCharsToAppendOrInsert).Fill(' '); 799 | _length = (short)Math.Min(endPositionFill + length, MaxLength); 800 | } 801 | } 802 | } 803 | } 804 | 805 | /// 806 | /// Represents a fixed-length string of maximum 63 characters. 807 | /// 808 | [InterpolatedStringHandler] 809 | public struct FixedString64 : IFixedString 810 | { 811 | /// 812 | public static int MaxLength 813 | { 814 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 815 | get => 63; 816 | } 817 | 818 | private short _length; 819 | #pragma warning disable CS0169 // Field is never used 820 | // Use short instead of char to make this struct blittable 821 | private short _c0; 822 | private short _c1; 823 | private short _c2; 824 | private short _c3; 825 | private short _c4; 826 | private short _c5; 827 | private short _c6; 828 | private short _c7; 829 | private short _c8; 830 | private short _c9; 831 | private short _c10; 832 | private short _c11; 833 | private short _c12; 834 | private short _c13; 835 | private short _c14; 836 | private short _c15; 837 | private short _c16; 838 | private short _c17; 839 | private short _c18; 840 | private short _c19; 841 | private short _c20; 842 | private short _c21; 843 | private short _c22; 844 | private short _c23; 845 | private short _c24; 846 | private short _c25; 847 | private short _c26; 848 | private short _c27; 849 | private short _c28; 850 | private short _c29; 851 | private short _c30; 852 | private short _c31; 853 | private short _c32; 854 | private short _c33; 855 | private short _c34; 856 | private short _c35; 857 | private short _c36; 858 | private short _c37; 859 | private short _c38; 860 | private short _c39; 861 | private short _c40; 862 | private short _c41; 863 | private short _c42; 864 | private short _c43; 865 | private short _c44; 866 | private short _c45; 867 | private short _c46; 868 | private short _c47; 869 | private short _c48; 870 | private short _c49; 871 | private short _c50; 872 | private short _c51; 873 | private short _c52; 874 | private short _c53; 875 | private short _c54; 876 | private short _c55; 877 | private short _c56; 878 | private short _c57; 879 | private short _c58; 880 | private short _c59; 881 | private short _c60; 882 | private short _c61; 883 | private short _c62; 884 | #pragma warning restore CS0169 // Field is never used 885 | 886 | /// 887 | /// Initializes a new instance of the struct. 888 | /// 889 | public FixedString64(int literalLength, int formattedCount) 890 | { 891 | _length = 0; 892 | } 893 | 894 | /// 895 | /// Initializes a new instance of the struct. 896 | /// 897 | public FixedString64(string value) 898 | { 899 | _length = 0; 900 | AppendLiteral(value); 901 | } 902 | 903 | /// 904 | public readonly int Length 905 | { 906 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 907 | get => _length; 908 | } 909 | 910 | /// 911 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 912 | public void Clear() => _length = 0; 913 | 914 | /// 915 | /// Appends the string literal to this fixed string. 916 | /// 917 | [EditorBrowsable(EditorBrowsableState.Never)] 918 | public void AppendLiteral(ReadOnlySpan s) 919 | { 920 | if (_length == MaxLength) return; 921 | 922 | var span = AsRemainingSpan(); 923 | if (span.Length < s.Length) 924 | { 925 | s.Slice(0, span.Length).CopyTo(span); 926 | _length += (short)span.Length; 927 | } 928 | else 929 | { 930 | s.CopyTo(span); 931 | _length += (short)s.Length; 932 | } 933 | } 934 | 935 | /// 936 | /// Apppends the specified string to this fixed string. 937 | /// 938 | [EditorBrowsable(EditorBrowsableState.Never)] 939 | public void AppendFormatted(string t) => AppendLiteral(t); 940 | 941 | /// 942 | /// Apppends the specified string to this fixed string. 943 | /// 944 | [EditorBrowsable(EditorBrowsableState.Never)] 945 | public void AppendFormatted(string t, int alignment) 946 | { 947 | var startPosition = _length; 948 | AppendFormatted(t); 949 | AppendOrInsertAlignment(startPosition, alignment); 950 | } 951 | 952 | /// 953 | /// Apppends the formatted value to this fixed string. 954 | /// 955 | [EditorBrowsable(EditorBrowsableState.Never)] 956 | public void AppendFormatted(T t) where T : ISpanFormattable 957 | { 958 | var span = AsRemainingSpan(); 959 | t.TryFormat(span, out int charsWritten, new ReadOnlySpan(), null); 960 | _length += (short)charsWritten; 961 | } 962 | 963 | /// 964 | /// Apppends the formatted value to this fixed string. 965 | /// 966 | [EditorBrowsable(EditorBrowsableState.Never)] 967 | public void AppendFormatted(T value, string? format) where T : ISpanFormattable 968 | { 969 | var span = AsRemainingSpan(); 970 | value.TryFormat(span, out int charsWritten, format, null); 971 | _length += (short)charsWritten; 972 | } 973 | 974 | /// 975 | /// Apppends the formatted value to this fixed string. 976 | /// 977 | [EditorBrowsable(EditorBrowsableState.Never)] 978 | public void AppendFormatted(T value, int alignment) where T : ISpanFormattable 979 | { 980 | var startPosition = _length; 981 | AppendFormatted(value); 982 | AppendOrInsertAlignment(startPosition, alignment); 983 | } 984 | 985 | /// 986 | /// Apppends the formatted value to this fixed string. 987 | /// 988 | [EditorBrowsable(EditorBrowsableState.Never)] 989 | public void AppendFormatted(T value, int alignment, string? format) where T : ISpanFormattable 990 | { 991 | var startPosition = _length; 992 | AppendFormatted(value, format); 993 | AppendOrInsertAlignment(startPosition, alignment); 994 | } 995 | 996 | /// } 997 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 998 | public bool Equals(FixedString64 other) => _length == other._length && AsSpan().SequenceEqual(other.AsSpan()); 999 | 1000 | /// 1001 | public override bool Equals(object? obj) => obj is FixedString64 other && Equals(other); 1002 | 1003 | /// 1004 | public override int GetHashCode() 1005 | { 1006 | // Compute the FNV-1a hash of the string 1007 | int hash = unchecked((int)2166136261); 1008 | foreach (var c in AsSpan()) 1009 | { 1010 | hash ^= c; 1011 | hash *= 16777619; 1012 | } 1013 | 1014 | return hash; 1015 | } 1016 | 1017 | /// 1018 | /// Returns a span of characters that contains the characters of this string. 1019 | /// 1020 | /// A span of characters that contains the characters of this string. 1021 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1022 | [UnscopedRef] 1023 | public readonly ReadOnlySpan AsSpan() => MemoryMarshal.CreateSpan(ref Unsafe.As(ref Unsafe.AsRef(_c0)), _length); 1024 | 1025 | Span IFixedString.GetUnsafeFullSpan() => AsUnsafeFullSpan(); 1026 | 1027 | /// 1028 | /// Implicit conversion from to . 1029 | /// 1030 | [SkipLocalsInit] 1031 | public static implicit operator FixedString64(string s) => new(s); 1032 | 1033 | /// 1034 | public readonly override string ToString() => ToString(null, null); 1035 | 1036 | /// 1037 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1038 | public readonly string ToString(string? format, IFormatProvider? formatProvider) => _length == 0 ? string.Empty : new(AsSpan()); 1039 | 1040 | /// 1041 | [EditorBrowsable(EditorBrowsableState.Never)] 1042 | public readonly bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) 1043 | { 1044 | if (destination.Length < _length) 1045 | { 1046 | charsWritten = 0; 1047 | return false; 1048 | } 1049 | AsSpan().CopyTo(destination); 1050 | charsWritten = _length; 1051 | return true; 1052 | } 1053 | 1054 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1055 | private Span AsUnsafeFullSpan() => MemoryMarshal.CreateSpan(ref Unsafe.As(ref _c0), MaxLength); 1056 | 1057 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1058 | [UnscopedRef] 1059 | private Span AsRemainingSpan() => AsUnsafeFullSpan().Slice(_length); 1060 | 1061 | /// 1062 | /// Appends or inserts the specified alignment at the specified position. 1063 | /// 1064 | private void AppendOrInsertAlignment(int startPosition, int alignment) 1065 | { 1066 | if (alignment == 0) return; 1067 | 1068 | int length = _length - startPosition; 1069 | bool padAfter = false; 1070 | if (alignment < 0) 1071 | { 1072 | padAfter = true; 1073 | alignment = -alignment; 1074 | } 1075 | 1076 | int numberOfCharsToAppendOrInsert = alignment - length; 1077 | if (numberOfCharsToAppendOrInsert <= 0) return; 1078 | 1079 | if (padAfter) 1080 | { 1081 | numberOfCharsToAppendOrInsert = Math.Min(MaxLength, _length + numberOfCharsToAppendOrInsert) - _length; 1082 | 1083 | if (numberOfCharsToAppendOrInsert > 0) 1084 | { 1085 | AsRemainingSpan().Slice(0, numberOfCharsToAppendOrInsert).Fill(' '); 1086 | _length += (short)numberOfCharsToAppendOrInsert; 1087 | } 1088 | } 1089 | else 1090 | { 1091 | numberOfCharsToAppendOrInsert = Math.Min(MaxLength, startPosition + numberOfCharsToAppendOrInsert) - startPosition; 1092 | 1093 | if (numberOfCharsToAppendOrInsert > 0) 1094 | { 1095 | var endPositionFill = startPosition + numberOfCharsToAppendOrInsert; 1096 | 1097 | var span = AsUnsafeFullSpan(); 1098 | if (endPositionFill < MaxLength) 1099 | { 1100 | var maxLengthToCopy = Math.Min(MaxLength, endPositionFill + length) - endPositionFill; 1101 | if (maxLengthToCopy > 0) 1102 | { 1103 | span.Slice(startPosition, maxLengthToCopy).CopyTo(span.Slice(endPositionFill, maxLengthToCopy)); 1104 | } 1105 | } 1106 | 1107 | span.Slice(startPosition, numberOfCharsToAppendOrInsert).Fill(' '); 1108 | _length = (short)Math.Min(endPositionFill + length, MaxLength); 1109 | } 1110 | } 1111 | } 1112 | } 1113 | 1114 | -------------------------------------------------------------------------------- /src/FixedStrings/FixedStrings.tt: -------------------------------------------------------------------------------- 1 | <#@ template debug="false" hostspecific="false" language="C#" #> 2 | <#@ assembly name="System.Core" #> 3 | <#@ import namespace="System.Linq" #> 4 | <#@ import namespace="System.Text" #> 5 | <#@ import namespace="System.Collections.Generic" #> 6 | <#@ output extension=".g.cs" #> 7 | // Copyright (c) Alexandre Mutel. All rights reserved. 8 | // Licensed under the BSD-Clause 2 license. 9 | // See license.txt file in the project root for full license information. 10 | using System; 11 | using System.ComponentModel; 12 | using System.Diagnostics.CodeAnalysis; 13 | using System.Runtime.CompilerServices; 14 | using System.Runtime.InteropServices; 15 | 16 | #nullable enable 17 | 18 | namespace FixedStrings; 19 | 20 | <# foreach(var size in new int[] {8, 16, 32, 64 }) { #> 21 | /// 22 | /// Represents a fixed-length string of maximum <#= size - 1 #> characters. 23 | /// 24 | [InterpolatedStringHandler] 25 | public struct FixedString<#= size #> : IFixedString> 26 | { 27 | /// 28 | public static int MaxLength 29 | { 30 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 31 | get => <#= size - 1 #>; 32 | } 33 | 34 | private short _length; 35 | #pragma warning disable CS0169 // Field is never used 36 | // Use short instead of char to make this struct blittable 37 | <# for(int i = 0; i < (size - 1); i++) { #> 38 | private short _c<#= i #>; 39 | <# } #> 40 | #pragma warning restore CS0169 // Field is never used 41 | 42 | /// 43 | /// Initializes a new instance of the struct. 44 | /// 45 | public FixedString<#= size #>(int literalLength, int formattedCount) 46 | { 47 | _length = 0; 48 | } 49 | 50 | /// 51 | /// Initializes a new instance of the struct. 52 | /// 53 | public FixedString<#= size #>(string value) 54 | { 55 | _length = 0; 56 | AppendLiteral(value); 57 | } 58 | 59 | /// 60 | public readonly int Length 61 | { 62 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 63 | get => _length; 64 | } 65 | 66 | /// 67 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 68 | public void Clear() => _length = 0; 69 | 70 | /// 71 | /// Appends the string literal to this fixed string. 72 | /// 73 | [EditorBrowsable(EditorBrowsableState.Never)] 74 | public void AppendLiteral(ReadOnlySpan s) 75 | { 76 | if (_length == MaxLength) return; 77 | 78 | var span = AsRemainingSpan(); 79 | if (span.Length < s.Length) 80 | { 81 | s.Slice(0, span.Length).CopyTo(span); 82 | _length += (short)span.Length; 83 | } 84 | else 85 | { 86 | s.CopyTo(span); 87 | _length += (short)s.Length; 88 | } 89 | } 90 | 91 | /// 92 | /// Apppends the specified string to this fixed string. 93 | /// 94 | [EditorBrowsable(EditorBrowsableState.Never)] 95 | public void AppendFormatted(string t) => AppendLiteral(t); 96 | 97 | /// 98 | /// Apppends the specified string to this fixed string. 99 | /// 100 | [EditorBrowsable(EditorBrowsableState.Never)] 101 | public void AppendFormatted(string t, int alignment) 102 | { 103 | var startPosition = _length; 104 | AppendFormatted(t); 105 | AppendOrInsertAlignment(startPosition, alignment); 106 | } 107 | 108 | /// 109 | /// Apppends the formatted value to this fixed string. 110 | /// 111 | [EditorBrowsable(EditorBrowsableState.Never)] 112 | public void AppendFormatted(T t) where T : ISpanFormattable 113 | { 114 | var span = AsRemainingSpan(); 115 | t.TryFormat(span, out int charsWritten, new ReadOnlySpan(), null); 116 | _length += (short)charsWritten; 117 | } 118 | 119 | /// 120 | /// Apppends the formatted value to this fixed string. 121 | /// 122 | [EditorBrowsable(EditorBrowsableState.Never)] 123 | public void AppendFormatted(T value, string? format) where T : ISpanFormattable 124 | { 125 | var span = AsRemainingSpan(); 126 | value.TryFormat(span, out int charsWritten, format, null); 127 | _length += (short)charsWritten; 128 | } 129 | 130 | /// 131 | /// Apppends the formatted value to this fixed string. 132 | /// 133 | [EditorBrowsable(EditorBrowsableState.Never)] 134 | public void AppendFormatted(T value, int alignment) where T : ISpanFormattable 135 | { 136 | var startPosition = _length; 137 | AppendFormatted(value); 138 | AppendOrInsertAlignment(startPosition, alignment); 139 | } 140 | 141 | /// 142 | /// Apppends the formatted value to this fixed string. 143 | /// 144 | [EditorBrowsable(EditorBrowsableState.Never)] 145 | public void AppendFormatted(T value, int alignment, string? format) where T : ISpanFormattable 146 | { 147 | var startPosition = _length; 148 | AppendFormatted(value, format); 149 | AppendOrInsertAlignment(startPosition, alignment); 150 | } 151 | 152 | /// } 153 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 154 | public bool Equals(FixedString<#= size #> other) => _length == other._length && AsSpan().SequenceEqual(other.AsSpan()); 155 | 156 | /// 157 | public override bool Equals(object? obj) => obj is FixedString<#= size #> other && Equals(other); 158 | 159 | /// 160 | public override int GetHashCode() 161 | { 162 | // Compute the FNV-1a hash of the string 163 | int hash = unchecked((int)2166136261); 164 | foreach (var c in AsSpan()) 165 | { 166 | hash ^= c; 167 | hash *= 16777619; 168 | } 169 | 170 | return hash; 171 | } 172 | 173 | /// 174 | /// Returns a span of characters that contains the characters of this string. 175 | /// 176 | /// A span of characters that contains the characters of this string. 177 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 178 | [UnscopedRef] 179 | public readonly ReadOnlySpan AsSpan() => MemoryMarshal.CreateSpan(ref Unsafe.As(ref Unsafe.AsRef(_c0)), _length); 180 | 181 | Span IFixedString.GetUnsafeFullSpan() => AsUnsafeFullSpan(); 182 | 183 | /// 184 | /// Implicit conversion from to . 185 | /// 186 | [SkipLocalsInit] 187 | public static implicit operator FixedString<#= size #>(string s) => new(s); 188 | 189 | /// 190 | public readonly override string ToString() => ToString(null, null); 191 | 192 | /// 193 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 194 | public readonly string ToString(string? format, IFormatProvider? formatProvider) => _length == 0 ? string.Empty : new(AsSpan()); 195 | 196 | /// 197 | [EditorBrowsable(EditorBrowsableState.Never)] 198 | public readonly bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) 199 | { 200 | if (destination.Length < _length) 201 | { 202 | charsWritten = 0; 203 | return false; 204 | } 205 | AsSpan().CopyTo(destination); 206 | charsWritten = _length; 207 | return true; 208 | } 209 | 210 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 211 | private Span AsUnsafeFullSpan() => MemoryMarshal.CreateSpan(ref Unsafe.As(ref _c0), MaxLength); 212 | 213 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 214 | [UnscopedRef] 215 | private Span AsRemainingSpan() => AsUnsafeFullSpan().Slice(_length); 216 | 217 | /// 218 | /// Appends or inserts the specified alignment at the specified position. 219 | /// 220 | private void AppendOrInsertAlignment(int startPosition, int alignment) 221 | { 222 | if (alignment == 0) return; 223 | 224 | int length = _length - startPosition; 225 | bool padAfter = false; 226 | if (alignment < 0) 227 | { 228 | padAfter = true; 229 | alignment = -alignment; 230 | } 231 | 232 | int numberOfCharsToAppendOrInsert = alignment - length; 233 | if (numberOfCharsToAppendOrInsert <= 0) return; 234 | 235 | if (padAfter) 236 | { 237 | numberOfCharsToAppendOrInsert = Math.Min(MaxLength, _length + numberOfCharsToAppendOrInsert) - _length; 238 | 239 | if (numberOfCharsToAppendOrInsert > 0) 240 | { 241 | AsRemainingSpan().Slice(0, numberOfCharsToAppendOrInsert).Fill(' '); 242 | _length += (short)numberOfCharsToAppendOrInsert; 243 | } 244 | } 245 | else 246 | { 247 | numberOfCharsToAppendOrInsert = Math.Min(MaxLength, startPosition + numberOfCharsToAppendOrInsert) - startPosition; 248 | 249 | if (numberOfCharsToAppendOrInsert > 0) 250 | { 251 | var endPositionFill = startPosition + numberOfCharsToAppendOrInsert; 252 | 253 | var span = AsUnsafeFullSpan(); 254 | if (endPositionFill < MaxLength) 255 | { 256 | var maxLengthToCopy = Math.Min(MaxLength, endPositionFill + length) - endPositionFill; 257 | if (maxLengthToCopy > 0) 258 | { 259 | span.Slice(startPosition, maxLengthToCopy).CopyTo(span.Slice(endPositionFill, maxLengthToCopy)); 260 | } 261 | } 262 | 263 | span.Slice(startPosition, numberOfCharsToAppendOrInsert).Fill(' '); 264 | _length = (short)Math.Min(endPositionFill + length, MaxLength); 265 | } 266 | } 267 | } 268 | } 269 | 270 | <# } #> -------------------------------------------------------------------------------- /src/FixedStrings/IFixedString.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Alexandre Mutel. All rights reserved. 2 | // Licensed under the BSD-Clause 2 license. 3 | // See license.txt file in the project root for full license information. 4 | 5 | using System; 6 | 7 | namespace FixedStrings; 8 | 9 | /// 10 | /// The base interface for a fixed string. 11 | /// 12 | public interface IFixedString : ISpanFormattable 13 | { 14 | /// 15 | /// Gets the maximum number of characters in this fixed string. 16 | /// 17 | static abstract int MaxLength { get; } 18 | 19 | /// 20 | /// Gets the number of characters in the string. The length is always less than or equal to . 21 | /// 22 | int Length { get; } 23 | 24 | /// 25 | /// Resets this string to an empty string. 26 | /// 27 | void Clear(); 28 | 29 | /// 30 | /// Returns a span of all the characters up to . 31 | /// 32 | /// A span of characters that contains the characters of this string. 33 | /// This method is unsafe as it doesn't protect from returning a ref to a struct that could go out of scope. Favor using AsSpan() on individual FixedStrings structs. 34 | /// This method can only be used through a generic constraint or by explicit casting/boxing to . 35 | /// 36 | Span GetUnsafeFullSpan(); 37 | } 38 | 39 | /// 40 | /// The base interface for a fixed string with its implementation type. 41 | /// 42 | public interface IFixedString : IFixedString, IEquatable where T: IFixedString 43 | { 44 | /// 45 | /// Converts a string to a fixed string. 46 | /// 47 | /// The string to convert to a fixed string. 48 | static abstract implicit operator T(string s); 49 | } 50 | -------------------------------------------------------------------------------- /src/dotnet-releaser.toml: -------------------------------------------------------------------------------- 1 | # configuration file for dotnet-releaser 2 | # Disable default packs - It will only publish NuGet and the Changelog 3 | [msbuild] 4 | project = "FixedStrings.sln" 5 | [github] 6 | user = "xoofx" 7 | repo = "FixedStrings" 8 | [coverage] 9 | badge_upload_to_gist = true 10 | badge_gist_id = "4b1dc8d0fa14dd6a3846e78e5f0eafae" -------------------------------------------------------------------------------- /src/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.100-rc.1.23463.5", 4 | "rollForward": "latestMinor" 5 | } 6 | } --------------------------------------------------------------------------------