├── .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 |
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 [](https://github.com/xoofx/FixedStrings/actions/workflows/ci.yml) [](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 | }
--------------------------------------------------------------------------------