Document Details

EventfulFable6349

Uploaded by EventfulFable6349

Andrew Troelsen, Phil Japikse

Tags

C# programming .NET platform software development programming language

Summary

This document introduces C# and the .NET 6 platform. It details the syntax and semantics of C# and illustrates .NET development frameworks. The document also covers concepts like assemblies, CIL, and JIT compilation, and provides an overview of .NET base class libraries.

Full Transcript

CHAPTER 1 Introducing C# and.NET 6 Microsoft’s.NET platform and the C# programming language were formally introduced circa 2002 and have quickly become a mainstay of modern-day software development. The.NET platform enables a large number of programming languages (including C#, VB.NET, and F#) to...

CHAPTER 1 Introducing C# and.NET 6 Microsoft’s.NET platform and the C# programming language were formally introduced circa 2002 and have quickly become a mainstay of modern-day software development. The.NET platform enables a large number of programming languages (including C#, VB.NET, and F#) to interact with each other. A program written in C# can be referenced by another program written in VB.NET. More on this interoperability later in this chapter. In 2016, Microsoft officially launched.NET Core. Like.NET,.NET Core allows languages to interop with each other (although a limited number of languages are supported). More importantly, this new framework is no longer limited to running on the Windows operating system but can also run on iOS, and Linux and be developed on MacOS and Linux. This platform independence opened up.NET and C# to a much larger pool of developers. While cross-platform use of C# was supported prior to.NET Core, that was through various other frameworks such as the Mono project. Note With the release of.NET 5, the “Core” part of the name was dropped. Throughout this book, the term.NET refers to.NET Core (up to 3.1) and.NET 5/6 Microsoft launched C# 10 and.NET 6 on November 8, 2021. C# 10 is tied to a specific version of the framework and will run only on.NET 6 and above. This relationship between language and.NET versions gives the C# team the freedom to introduce features into C# that couldn’t otherwise be added into the language due to framework limitations. As mentioned in the book’s introduction, the goal of this text is twofold. The first order of business is to provide you with a deep and detailed examination of the syntax and semantics of C#. The second (equally important) order of business is to illustrate the use of numerous.NET development frameworks. These include database access with ADO.NET and Entity Framework (EF) Core, user interfaces with Windows Presentation Foundation (WPF), and finally RESTful services and web applications with ASP.NET Core. As it is said, the journey of a thousand miles begins with a single step; and with this, I welcome you to Chapter 1. This first chapter lays the conceptual groundwork for the remainder of the book. Here, you will find a high-level discussion of a number of.NET-related topics such as assemblies, the Common Intermediate Language (CIL), and just-in-time (JIT) compilation. In addition to previewing some keywords of the C# programming language, you will also come to understand the relationship between the.NET Runtime, the Common Type System (CTS) and the Common Language Specification (CLS). This chapter also provides you with a survey of the functionality supplied by the.NET base class libraries, sometimes abbreviated as BCLs. Here, you will get an overview of the language-agnostic and platform-independent nature of the.NET platform. As you would expect, these topics are explored in further detail throughout the remainder of this text. © Andrew Troelsen, Phil Japikse 2022 3 A. Troelsen and P. Japikse, Pro C# 10 with.NET 6, https://doi.org/10.1007/978-1-4842-7869-7_1 Chapter 1 Introducing C# and.NET 6 Note Many of the features highlighted in this chapter (and throughout the book) also apply to the original.NET Framework. Exploring Some Key Benefits of the.NET Platform The.NET framework is a software platform for building web applications and services for the Windows, iOS, and Linux operating systems, as well as WinForms and WPF applications on Windows operating systems. To set the stage, here is a quick rundown of some core features provided, courtesy of.NET: Support for numerous programming languages:.NET applications can be created using C#, F#, and VB.NET programming languages (with C# and F# being the primary languages for ASP.NET Core). A common runtime engine shared by all.NET languages: One aspect of this engine is a well-defined set of types that each.NET language understands. Language integration:.NET supports cross-language inheritance, cross-language exception handling, and cross-language debugging of code. For example, you can define a base class in C# and extend this type in Visual Basic. A comprehensive base class library: This library provides thousands of predefined types that allow you to build code libraries, simple terminal applications, graphical desktop applications, and enterprise-level websites. A simplified deployment model:.NET libraries are not registered into the system registry. Furthermore, the.NET platform allows multiple versions of the framework as well as applications to exist in harmony on a single machine. Extensive command-line support: The.NET command-line interface (CLI) is a cross- platform tool chain for developing and packaging.NET applications. Additional tools can be installed (globally or locally) beyond the standard tools that ship with the.NET SDK. You will see each of these topics (and many more) examined in the chapters to come. But first, I need to explain the new support lifecycle for.NET. Understanding the.NET Support Lifecycle.NET versions are released much more frequently than prior.NET Framework. With all of these releases available, it can be difficult to keep up, especially in an enterprise development environment. To better define the support lifecycle for the releases, Microsoft has adopted a variation of the Long-Term Support Model,1 commonly used by modern open source frameworks. Long-Term Support (LTS) releases are major releases that will be supported for an extended period of time. They will only receive critical and/or nonbreaking fixes throughout their life span. Prior to end-of-life, LTS versions will be changed to the designation of maintenance. LTS releases with.NET will be supported for the following time frames, whichever is longer: Three years after initial release One year of maintenance support after subsequent LTS release 1 https://en.wikipedia.org/wiki/Long-term_support 4 Chapter 1 Introducing C# and.NET 6 Microsoft has decided to name Short-Term Support releases as Current, which are interval releases between the major LTS releases. They are supported for six months after a subsequent Current or LTS release. As mentioned earlier,.NET 6 was released in November 2021. It was released as a Long Term Support version and will be supported at least until November 2024..NET 5 will go out of support on May 8, 2022, six months after the.NET 6 release. The only other supported version at the time of this writing is.NET Core 3.1 which is supported until December 3, 2022. It’s important to check the support policy for each new version of.NET that is released. Just having a higher number doesn’t necessarily mean it’s going to be supported long term. The full policy is located here: https://dotnet.microsoft.com/platform/support-­policy/dotnet-­core Previewing the Building Blocks of the.NET Platform Now that you know some of the major benefits provided by.NET, let’s preview key (and interrelated) topics that make it all possible: the.NET Runtime, CTS, and the CLS. From a programmer’s point of view,.NET can be understood as a runtime environment and a comprehensive base class library. The runtime layer contains the set of minimal implementations that are tied specifically to a platform (Windows, iOS, Linux) and architecture (x86, x64, ARM), as well as all of the base types for.NET. Another building block of the.NET platform is the Common Type System, or CTS. The CTS specification fully describes all possible data types and all programming constructs supported by the runtime, specifies how these entities can interact with each other, and details how they are represented in the.NET metadata format (more information on metadata later in this chapter; see Chapter 17 for complete details). Understand that a given.NET language might not support every feature defined by the CTS. The Common Language Specification, or CLS, is a related specification that defines a subset of common types and programming constructs that all.NET programming languages can agree on. Thus, if you build.NET types that expose only CLS-compliant features, you can rest assured that all.NET languages can consume them. Conversely, if you make use of a data type or programming construct that is outside of the bounds of the CLS, you cannot guarantee that every.NET programming language can interact with your.NET code library. Thankfully, as you will see later in this chapter, it is simple to tell your C# compiler to check all of your code for CLS compliance. The Role of the Base Class Libraries The.NET platform also provides a set of base class libraries (BCLs) that are available to all.NET programming languages. Not only does this base class library encapsulate various primitives such as threads, file input/output (I/O), graphical rendering systems, and interaction with various external hardware devices, but it also provides support for a number of services required by most real-world applications. The base class libraries define types that can be used to build any type of software application and for components of the application to interact with each other. The Role of.NET Standard The number of base class libraries in the.NET Framework far exceeds those in.NET. This is understandable, as the.NET Framework had a 14-year head start on.NET. This disparity created issues when attempting to use.NET Framework code with.NET code. The solution (and requirement) for.NET Framework/.NET Core 3.1 interop is.NET Standard. 5 Chapter 1 Introducing C# and.NET 6.NET Standard is a specification that defines the availability of.NET APIs and base class libraries that must be available in each implementation. The standard enables the following scenarios: Defines a uniform set of BCL APIs for all.NET implementations to implement, independent of workload Enables developers to produce portable libraries that are usable across.NET implementations, using this same set of APIs Reduces or even eliminates conditional compilation of shared source due to.NET APIs, only for OS APIs The chart located in the Microsoft documentation (https://docs.microsoft.com/en-­us/dotnet/ standard/net-­standard) shows the various compatibility between.NET Framework and.NET. This is useful for prior versions of C#. However, C# 9+ will only run on.NET 5+ or.NET Standard 2.1, and.NET Standard 2.1 is not available to the.NET Framework. What C# Brings to the Table C# is a programming language whose core syntax looks very similar to the syntax of Java. However, calling C# a Java clone is inaccurate. In reality, both C# and Java are members of the C family of programming languages (e.g., C, Objective-C, C++) and, therefore, share a similar syntax. The truth of the matter is that many of C#’s syntactic constructs are modeled after various aspects of Visual Basic (VB) and C++. For example, like VB, C# supports the notion of class properties (as opposed to traditional getter and setter methods) and optional parameters. Like C++, C# allows you to overload operators, as well as create structures, enumerations, and callback functions (via delegates). Moreover, as you work through this text, you will quickly see that C# supports a number of features, such as lambda expressions and anonymous types, traditionally found in various functional languages (e.g., LISP or Haskell). Furthermore, with the advent of Language Integrated Query (LINQ), C# supports a number of constructs that make it quite unique in the programming landscape. Nevertheless, the bulk of C# is indeed influenced by C-based languages. Because C# is a hybrid of numerous languages, the result is a product that is as syntactically clean as (if not cleaner than) Java provides just about as much power and flexibility as C++. Here is a partial list of core C# features that are found in all versions of the language: No pointers required! C# programs typically have no need for direct pointer manipulation (although you are free to drop down to that level if absolutely necessary, as shown in Chapter 11). Automatic memory management through garbage collection. Given this, C# does not support a delete keyword. Formal syntactic constructs for classes, interfaces, structures, enumerations, and delegates. The C++-like ability to overload operators for a custom type, without the complexity. Support for attribute-based programming. This brand of development allows you to annotate types and their members to further qualify their behavior. For example, if you mark a method with the [Obsolete] attribute, programmers will see your custom warning message print out if they attempt to make use of the decorated member. C# 10 is an already powerful language and, combined with.NET, enables building a wide range of application types. 6 Chapter 1 Introducing C# and.NET 6 Major Features in Prior Releases Starting with C# 7, I started adding into the section headers the version when features were added (e.g. “(New 7.x)”) or updated (e.g. “(Updated 7.x)”). New Features in C# 10 C# 10, released on November 8, 2021, with.NET 6, adds the following features: Record structs Improvements to structure types Global using directives and global implicit using directives File scoped namespaces Property pattern matching enhancements Constant Interpolated strings Improvements to lambda expressions Record type enhancements Assignment and declaration in deconstruction Removal of false warnings on definite assignment In addition to this, there are also updates to the.NET framework that impact development. I have updated the section headers that are new (or updated) in C#10 /NET 6 with “(New 10)” or “(Updated 10”). Managed vs. Unmanaged Code It is important to note that the C# language can be used only to build software that is hosted under the.NET runtime (you could never use C# to build a native COM server or an unmanaged C/C++-style application). Officially speaking, the term used to describe the code targeting the.NET runtime is managed code. The binary unit that contains the managed code is termed an assembly (more details on assemblies in just a bit). Conversely, code that cannot be directly hosted by the.NET runtime is termed unmanaged code. As mentioned previously, the.NET platform can run on a variety of operating systems. Thus, it is quite possible to build a C# application on a Windows machine and run the program on an iOS machine using the.NET runtime. As well, you can build a C# application on Linux using Visual Studio Code and run the program on Windows. With Visual Studio for Mac, you can also build.NET applications on a Mac to be run on Windows, macOS, or Linux. Unmanaged code can still be accessed from a C# program, but it then locks you into a specific development and deployment target. Using Additional.NET–Aware Programming Languages Understand that C# is not the only language that can be used to build.NET applications..NET applications can generally be built with C#, Visual Basic, and F#, which are the three languages supported directly by Microsoft. 7 Chapter 1 Introducing C# and.NET 6 Getting an Overview of.NET Assemblies Regardless of which.NET language you choose to program with, understand that despite.NET binaries taking the same file extension as unmanaged Windows binaries (*.dll), they have absolutely no internal similarities. Specifically,.NET binaries do not contain platform-specific instructions but rather platform- agnostic Intermediate Language (IL) and type metadata. Note IL is also known as Microsoft Intermediate Language (MSIL) or alternatively as the Common Intermediate Language (CIL). Thus, as you read the.NET literature, understand that IL, MSIL, and CIL are all describing essentially the same concept. In this book, I will use the abbreviation CIL to refer to this low-level instruction set. When a *.dll has been created using a.NET compiler, the binary blob is termed an assembly. You will examine numerous details of.NET assemblies in Chapter 16. However, to facilitate the current discussion, you do need to understand four basic properties of this new file format. First, unlike.NET Framework assemblies that can be either a *.dll or *.exe,.NET projects are always compiled to a file with a.dll extension, even if the project is an executable. Executable.NET assemblies are executed with the command dotnet.dll. New in.NET Core 3.0 (and later), the dotnet.exe command is copied to the build directory and renamed to.exe. Running this command automatically calls the dotnet.dll file, executing the equivalent of dotnet.dll. The *.exe with your project name isn’t actually your project’s code; it is a convenient shortcut to running your application. Updated in.NET 6, your application can be reduced to a single file that is executed directly. Even though this single file looks and acts like a C++-style native executable, the single file is a packaging convenience. It contains all the files needed to run your application, potentially even the.NET runtime itself! But know that your code is still running in a managed container just as if it were published as multiple files. Second, an assembly contains CIL code, which is conceptually similar to Java bytecode, in that it is not compiled to platform-specific instructions until absolutely necessary. Typically, “absolutely necessary” is the point at which a block of CIL instructions (such as a method implementation) is referenced for use by the.NET runtime. Third, assemblies also contain metadata that describes in vivid detail the characteristics of every “type” within the binary. For example, if you have a class named SportsCar, the type metadata describes details such as SportsCar’s base class, specifies which interfaces are implemented by SportsCar (if any), and gives full descriptions of each member supported by the SportsCar type..NET metadata is always present within an assembly and is automatically generated by the language compiler. Finally, in addition to CIL and type metadata, assemblies themselves are also described using metadata, which is officially termed a manifest. The manifest contains information about the current version of the assembly, culture information (used for localizing string and image resources), and a list of all externally referenced assemblies that are required for proper execution. You’ll examine various tools that can be used to examine an assembly’s types, metadata, and manifest information over the course of the next few chapters. The Role of the Common Intermediate Language Let’s examine CIL code, type metadata, and the assembly manifest in a bit more detail. CIL is a language that sits above any particular platform-specific instruction set. For example, the following C# code models a trivial calculator. Don’t concern yourself with the exact syntax for now, but do notice the format of the Add() method in the Calc class. 8 Chapter 1 Introducing C# and.NET 6 //Calc.cs Calc c = new Calc(); int ans = c.Add(10, 84); Console.WriteLine("10 + 84 is {0}.", ans); //Wait for user to press the Enter key Console.ReadLine(); // The C# calculator. class Calc {   public int Add(int addend1, int addend2)   {     return addend1 + addend2;   } } Compiling this code produces a file *.dll assembly that contains a manifest, CIL instructions, and metadata describing each aspect of the Calc and Program classes. Note Chapter 2 examines how to use graphical integrated development environments (IDEs), such as Visual Studio Community, to compile your code files. For example, if you were to output the IL from this assembly using ildasm.exe (examined later in this chapter), you would find that the Add() method is represented using CIL such as the following:   .method public hidebysig instance int32 Add(int32 addend1,               int32 addend2) cil managed   {     // Method begins at RVA 0x2090     // Code size       9 (0x9)     .maxstack  2     .locals init (int32 V_0)     IL_0000:   nop     IL_0001:   ldarg.1     IL_0002:   ldarg.2     IL_0003:   add     IL_0004:   stloc.0     IL_0005:   br.s       IL_0007     IL_0007:   ldloc.0     IL_0008:   ret   } // end of method Calc::Add Don’t worry if you are unable to make heads or tails of the resulting CIL for this method because Chapter 18 will describe the basics of the CIL programming language. The point to concentrate on is that the C# compiler emits CIL, not platform-specific instructions. Now, recall that this is true of all.NET compilers. To illustrate, assume you created this same application using Visual Basic, rather than C#. 9 Chapter 1 Introducing C# and.NET 6 ' Calc.vb Module Program   ' This class contains the app's entry point.   Sub Main(args As String())     Dim c As New Calc     Dim ans As Integer = c.Add(10, 84)     Console.WriteLine("10 + 84 is {0}", ans)     'Wait for user to press the Enter key before shutting down     Console.ReadLine()   End Sub End Module ' The VB.NET calculator. Class Calc   Public Function Add(ByVal addend1 As Integer, ByVal addend2 As Integer) As Integer     Return addend1 + addend2   End Function End Class If you examine the CIL for the Add() method, you find similar instructions (slightly tweaked by the Visual Basic compiler).   .method public instance int32  Add(int32 addend1,                                      int32 addend2) cil managed   {     // Code size       9 (0x9)     .maxstack  2     .locals init (int32 V_0)     IL_0000:  nop     IL_0001:  ldarg.1     IL_0002:  ldarg.2     IL_0003:  add.ovf     IL_0004:  stloc.0     IL_0005:  br.s       IL_0007     IL_0007:  ldloc.0     IL_0008:  ret   } // end of method Calc::Add As a final example, the same simple Calc program developed in F# (another.NET language) is shown here: // Learn more about F# at http://fsharp.org // Calc.fs open System module Calc =     let add addend1 addend2 =         addend1 + addend2 [] let main argv = 10 Chapter 1 Introducing C# and.NET 6     let ans = Calc.add 10 84     printfn "10 + 84 is %d" ans     Console.ReadLine()     0 If you examine the CIL for the Add() method, once again you find similar instructions (slightly tweaked by the F# compiler).   .method public instance int32  Add(int32 addend1,                                      int32 addend2) cil managed   {     // Code size       9 (0x9)     .maxstack  2     .locals init (int32 V_0)     IL_0000:  nop     IL_0001:  ldarg.1     IL_0002:  ldarg.2     IL_0003:  add.ovf     IL_0004:  stloc.0     IL_0005:  br.s       IL_0007     IL_0007:  ldloc.0     IL_0008:  ret   } // end of method Calc::Add } // end of class Calc.Vb.Calc Benefits of CIL At this point, you might be wondering exactly what is gained by compiling source code into CIL rather than directly to a specific instruction set. One benefit is language integration. As you have already seen, each.NET compiler produces nearly identical CIL instructions. Therefore, all languages are able to interact within a well-defined binary arena. Furthermore, given that CIL is platform-agnostic, the.NET Framework itself is platform-agnostic, providing the same benefits Java developers have grown accustomed to (e.g., a single code base running on numerous operating systems). In fact, there is an international standard for the C# language. Prior to.NET Core, there were numerous implementations of the.NET framework for non-Windows platforms, such as Mono. These still exist, although the release of.NET 6 greatly reduced the need for those other platforms. Compiling CIL to Platform-Specific Instructions Because assemblies contain CIL instructions rather than platform-specific instructions, CIL code must be compiled on the fly before use. The entity that compiles CIL code into meaningful CPU instructions is a JIT compiler, which sometimes goes by the friendly name of jitter. The.NET runtime environment leverages a JIT compiler for each CPU targeting the runtime, each optimized for the underlying platform. For example, if you are building a.NET application to be deployed to a handheld device (such as an iOS or Android phone), the corresponding jitter is well equipped to run within a low-memory environment. On the other hand, if you are deploying your assembly to a back-end company server (where memory is seldom an issue), the jitter will be optimized to function in a high-memory environment. In this way, developers can write a single body of code that can be efficiently JIT compiled and executed on machines with different architectures. 11 Chapter 1 Introducing C# and.NET 6 Furthermore, as a given jitter compiles CIL instructions into corresponding machine code, it will cache the results in memory in a manner suited to the target operating system. In this way, if a call is made to a method named PrintDocument(), the CIL instructions are compiled into platform-specific instructions on the first invocation and retained in memory for later use. Therefore, the next time PrintDocument() is called, there is no need to recompile the CIL. Precompiling CIL to Platform-Specific Instructions There is a utility in.NET called crossgen.exe, which can be used to pre-JIT your code. Fortunately, in.NET Core 6, the ability to produce “ready-to-run” assemblies is built into the framework. More on this later in this book. The Role of.NET Type Metadata In addition to CIL instructions, a.NET assembly contains full, complete, and accurate metadata, which describes every type (e.g., class, structure, enumeration) defined in the binary, as well as the members of each type (e.g., properties, methods, events). Thankfully, it is always the job of the compiler (not the programmer) to emit the latest and greatest type metadata. Because.NET metadata is so wickedly meticulous, assemblies are completely self-describing entities. To illustrate the format of.NET type metadata, let’s take a look at the metadata that has been generated for the Add() method of the C# Calc class you examined previously (the metadata generated for the Visual Basic version of the Add() method is similar, so we will examine the C# version only). // TypeDef #2 (02000003) // ------------------------------------------------------- //   TypDefName: Calc  (02000003) //   Flags     : [NotPublic] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit]  (00100000) //   Extends   : 0100000D [TypeRef] System.Object //   Method #1 (06000003) //   ------------------------------------------------------- //     MethodName: Add (06000003) //     Flags     : [Public] [HideBySig] [ReuseSlot]  (00000086) //     RVA      : 0x00002090 //     ImplFlags : [IL] [Managed]  (00000000) //     CallCnvntn: [DEFAULT] //     hasThis //     ReturnType: I4 //     2 Arguments //       Argument #1:  I4 //       Argument #2:  I4 //     2 Parameters //       (1) ParamToken : (08000002) Name : addend1 flags: [none] (00000000) //       (2) ParamToken : (08000003) Name : addend2 flags: [none] (00000000) Metadata is used by numerous aspects of the.NET runtime environment, as well as by various development tools. For example, the IntelliSense feature provided by tools such as Visual Studio is made possible by reading an assembly’s metadata at design time. Metadata is also used by various object-browsing utilities, debugging tools, and the C# compiler itself. To be sure, metadata is the backbone of numerous.NET technologies including reflection, late binding, and object serialization. Chapter 17 will formalize the role of.NET metadata. 12 Chapter 1 Introducing C# and.NET 6 The Role of the Assembly Manifest Last but not least, remember that a.NET assembly also contains metadata that describes the assembly itself (technically termed a manifest). Among other details, the manifest documents all external assemblies required by the current assembly to function correctly, the assembly’s version number, copyright information, and so forth. Like type metadata, it is always the job of the compiler to generate the assembly’s manifest. Here are some relevant details of the manifest generated when compiling the Calc.cs code file shown earlier in this chapter (some lines omitted for brevity):.assembly extern System.Runtime {   .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )                         //.?_....:   .ver 6:0:0:0 }.assembly extern System.Console {   .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )                         //.?_....:   .ver 6:0:0:0 }.assembly Calc.Cs {   .hash algorithm 0x00008004   .ver 1:0:0:0 }.module Calc.Cs.dll.imagebase 0x00400000.file alignment 0x00000200.stackreserve 0x00100000.subsystem 0x0003       // WINDOWS_CUI.corflags 0x00000001    //  ILONLY In a nutshell, the manifest documents the set of external assemblies required by Calc.dll (via the.assembly extern directive) as well as various characteristics of the assembly itself (e.g., version number, module name). Chapter 16 will examine the usefulness of manifest data in much more detail. Understanding the Common Type System A given assembly may contain any number of distinct types. In the world of.NET, type is simply a general term used to refer to a member from the set {class, interface, structure, enumeration, delegate}. When you build solutions using a.NET language, you will most likely interact with many of these types. For example, your assembly might define a single class that implements some number of interfaces. Perhaps one of the interface methods takes an enumeration type as an input parameter and returns a structure to the caller. Recall that the CTS is a formal specification that documents how types must be defined in order to be hosted by the.NET Runtime. Typically, the only individuals who are deeply concerned with the inner workings of the CTS are those building tools and/or compilers that target the.NET platform. It is important, however, for all.NET programmers to learn about how to work with the five types defined by the CTS in their language of choice. The following is a brief overview. 13 Chapter 1 Introducing C# and.NET 6 CTS Class Types Every.NET language supports, at the least, the notion of a class type, which is the cornerstone of object- oriented programming (OOP). A class may be composed of any number of members (such as constructors, properties, methods, and events) and data points (fields). In C#, classes are declared using the class keyword, like so: // A C# class type with 1 method. class Calc {   public int Add(int addend1, int addend2)   {     return addend1 + addend2;   } } Chapter 5 will begin your formal examination of building class types with C#; however, Table 1-1 documents a number of characteristics pertaining to class types. Table 1-1. CTS Class Characteristics Class Characteristic Meaning in Life Is the class sealed? Sealed classes cannot function as a base class to other classes. Does the class implement any An interface is a collection of abstract members that provides a contract interfaces? between the object and object user. The CTS allows a class to implement any number of interfaces. Is the class abstract or Abstract classes cannot be directly instantiated but are intended to define concrete? common behaviors for derived types. Concrete classes can be instantiated directly. What is the visibility of this Each class must be configured with a visibility keyword such as public class? or internal. Basically, this controls whether the class may be used by external assemblies or only from within the defining assembly. CTS Interface Types Interfaces are nothing more than a named collection of abstract member definitions and/or (introduced in C# 8) default implementations, which are implemented (optionally in the case of default implementations) by a given class or structure. In C#, interface types are defined using the interface keyword. By convention, all.NET interfaces begin with a capital letter I, as in the following example: // A C# interface type is usually // declared as public, to allow types in other // assemblies to implement their behavior. public interface IDraw {   void Draw(); } 14 Chapter 1 Introducing C# and.NET 6 On their own, interfaces are of little use. However, when a class or structure implements a given interface in its unique way, you are able to request access to the supplied functionality using an interface reference in a polymorphic manner. Interface-based programming will be fully explored in Chapter 8. CTS Structure Types The concept of a structure is also formalized under the CTS. If you have a C background, you should be pleased to know that these user-defined types (UDTs) have survived in the world of.NET (although they behave a bit differently under the hood). Simply put, a structure can be thought of as a lightweight class type having value-based semantics. For more details on the subtleties of structures, see Chapter 4. Typically, structures are best suited for modeling geometric and mathematical data and are created in C# using the struct keyword, as follows: // A C# structure type. struct Point {   // Structures can contain fields.   public int xPos, yPos;   // Structures can contain parameterized constructors.   public Point(int x, int y)   { xPos = x; yPos = y;}   // Structures may define methods.   public void PrintPosition()   {     Console.WriteLine("({0}, {1})", xPos, yPos);   } } CTS Enumeration Types Enumerations are a handy programming construct that allow you to group name-value pairs. For example, assume you are creating a video game application that allows the player to select from three character categories (Wizard, Fighter, or Thief ). Rather than keeping track of simple numerical values to represent each possibility, you could build a strongly typed enumeration using the enum keyword. // A C# enumeration type. enum CharacterTypeEnum {   Wizard = 100,   Fighter = 200,   Thief = 300 } By default, the storage used to hold each item is a 32-bit integer; however, it is possible to alter this storage slot if need be (e.g., when programming for a low-memory device such as a mobile device). Also, the CTS demands that enumerated types derive from a common base class, System.Enum. As you will see in Chapter 4, this base class defines a number of interesting members that allow you to extract, manipulate, and transform the underlying name-value pairs programmatically. 15 Chapter 1 Introducing C# and.NET 6 CTS Delegate Types Delegates are the.NET equivalent of a type-safe, C-style function pointer. The key difference is that a.NET delegate is a class that derives from System.MulticastDelegate, rather than a simple pointer to a raw memory address. In C#, delegates are declared using the delegate keyword. // This C# delegate type can "point to" any method // returning an int and taking two ints as input. delegate int BinaryOp(int x, int y); Delegates are critical when you want to provide a way for one object to forward a call to another object and provide the foundation for the.NET event architecture. As you will see in Chapters 12 and 14, delegates have intrinsic support for multicasting (i.e., forwarding a request to multiple recipients) and asynchronous method invocations (i.e., invoking the method on a secondary thread). CTS Type Members Now that you have previewed each of the types formalized by the CTS, realize that most types take any number of members. Formally speaking, a type member is constrained by the set {constructor, finalizer, static constructor, nested type, operator, method, property, indexer, field, read-only field, constant, event}. The CTS defines various adornments that may be associated with a given member. For example, each member has a given visibility trait (e.g., public, private, protected). Some members may be declared as abstract (to enforce a polymorphic behavior on derived types) as well as virtual (to define a canned, but overridable, implementation). Also, most members may be configured as static (bound at the class level) or instance (bound at the object level). The creation of type members is examined over the course of the next several chapters. Note As described in Chapter 10, the C# language also supports the creation of generic types and generic members. Intrinsic CTS Data Types The final aspect of the CTS to be aware of for the time being is that it establishes a well-defined set of fundamental data types. Although a given language typically has a unique keyword used to declare a fundamental data type, all.NET language keywords ultimately resolve to the same CTS type defined in an assembly named mscorlib.dll. Consider Table 1-2, which documents how key CTS data types are expressed in VB.NET and C#. 16 Chapter 1 Introducing C# and.NET 6 Table 1-2. The Intrinsic CTS Data Types CTS Data Type VB Keyword C# Keyword System.Byte Byte byte System.SByte SByte sbyte System.Int16 Short short System.Int32 Integer int System.Int64 Long long System.UInt16 UShort ushort System.UInt32 UInteger uint System.UInt64 ULong ulong System.Single Single float System.Double Double double System.Object Object object System.Char Char char System.String String string System.Decimal Decimal decimal System.Boolean Boolean bool Given that the unique keywords of a managed language are simply shorthand notations for a real type in the System namespace, you no longer have to worry about overflow/underflow conditions for numerical data or how strings and Booleans are internally represented across different languages. Consider the following code snippets, which define 32-bit numerical variables in C# and Visual Basic, using language keywords as well as the formal CTS data type: // Define some "ints" in C#. int i = 0; System.Int32 j = 0; ' Define some  "ints" in VB. Dim i As Integer = 0 Dim j As System.Int32 = 0 Understanding the Common Language Specification As you are aware, different languages express the same programming constructs in unique, language- specific terms. For example, in C# you denote string concatenation using the plus operator (+), while in VB you typically make use of the ampersand (&). Even when two distinct languages express the same programmatic idiom (e.g., a function with no return value), the chances are good that the syntax will appear quite different on the surface. 17 Chapter 1 Introducing C# and.NET 6 // C# method returning nothing. public void MyMethod() {   // Some interesting code... } ' VB method returning nothing. Public Sub MyMethod()   ' Some interesting code... End Sub As you have already seen, these minor syntactic variations are inconsequential in the eyes of the.NET runtime, given that the respective compilers (csc.exe or vbc.exe, in this case) emit a similar set of CIL instructions. However, languages can also differ with regard to their overall level of functionality. For example, a.NET language might or might not have a keyword to represent unsigned data and might or might not support pointer types. Given these possible variations, it would be ideal to have a baseline to which all.NET languages are expected to conform. The CLS is a set of rules that describe in vivid detail the minimal and complete set of features a given.NET compiler must support to produce code that can be hosted by the.NET Runtime, while at the same time be accessed in a uniform manner by all languages that target the.NET platform. In many ways, the CLS can be viewed as a subset of the full functionality defined by the CTS. The CLS is ultimately a set of rules that compiler builders must conform to if they intend their products to function seamlessly within the.NET universe. Each rule is assigned a simple name (e.g., CLS Rule 6) and describes how this rule affects those who build the compilers as well as those who (in some way) interact with them. The crème de la crème of the CLS is Rule 1. Rule 1: CLS rules apply only to those parts of a type that are exposed outside the defining assembly. Given this rule, you can (correctly) infer that the remaining rules of the CLS do not apply to the logic used to build the inner workings of a.NET type. The only aspects of a type that must conform to the CLS are the member definitions themselves (i.e., naming conventions, parameters, and return types). The implementation logic for a member may use any number of non-CLS techniques, as the outside world won’t know the difference. To illustrate, the following C# Add() method is not CLS compliant, as the parameters and return values make use of unsigned data (which is not a requirement of the CLS): class Calc {   // Exposed unsigned data is not CLS compliant!   public ulong Add(ulong addend1, ulong addend2)   {     return addend1 + addend2;   } } However, consider the following code that makes use of unsigned data internally in a method: class Calc {   public int Add(int addend1, int addend2)   {     // As this ulong variable is only used internally, 18 Chapter 1 Introducing C# and.NET 6     // we are still CLS compliant.     ulong temp = 0;     ...     return addend1 + addend2;   } } The class still conforms to the rules of the CLS and can rest assured that all.NET languages are able to invoke the Add() method. Of course, in addition to Rule 1, the CLS defines numerous other rules. For example, the CLS describes how a given language must represent text strings, how enumerations should be represented internally (the base type used for storage), how to define static members, and so forth. Luckily, you don’t have to commit these rules to memory to be a proficient.NET developer. Again, by and large, an intimate understanding of the CTS and CLS specifications is typically of interest only to tool/compiler builders. Ensuring CLS Compliance As you will see over the course of this book, C# does define a number of programming constructs that are not CLS compliant. The good news, however, is that you can instruct the C# compiler to check your code for CLS compliance using a single.NET attribute. // Tell the C# compiler to check for CLS compliance. [assembly: CLSCompliant(true)] Chapter 17 dives into the details of attribute-based programming. Until then, simply understand that the [CLSCompliant] attribute will instruct the C# compiler to check every line of code against the rules of the CLS. If any CLS violations are discovered, you receive a compiler warning and a description of the offending code. Understanding the.NET Runtime In addition to the CTS and CLS specifications, the final piece of the puzzle to contend with is the.NET Runtime. Programmatically speaking, the term runtime can be understood as a collection of services that are required to execute a given compiled unit of code. For example, when Java developers deploy software to a new computer, they need to ensure the Java virtual machine (JVM) has been installed on the machine in order to run their software. The.NET platform offers yet another runtime system. The key difference between the.NET runtime and the various other runtimes I just mentioned is that the.NET runtime provides a single, well-defined runtime layer that is shared by all languages and platforms that are.NET. Distinguishing Between Assembly, Namespace, and Type Each of us understands the importance of code libraries. The point of framework libraries is to give developers a well-defined set of existing code to leverage in their applications. However, the C# language does not come with a language-specific code library. Rather, C# developers leverage the language-neutral.NET libraries. To keep all the types within the base class libraries well organized, the.NET platform makes extensive use of the namespace concept. 19 Chapter 1 Introducing C# and.NET 6 A namespace is a grouping of semantically related types contained in an assembly or possibly spread across multiple related assemblies. For example, the System.IO namespace contains file I/O-related types, the System.Data namespace defines basic database types, and so on. It is important to point out that a single assembly can contain any number of namespaces, each of which can contain any number of types. The key difference between this approach and a language-specific library is that any language targeting the.NET runtime uses the same namespaces and same types. For example, the following two programs all illustrate the ubiquitous Hello World application, written in C# and VB: Note The following code uses the C# 9 version of the Program class with a void Main() method to help illustrate the example. The new templates in C# 10 use top level statements (covered in Chapter 3) and global implicit using statements (covered later in this chapter). // Hello World in C#. using System; public class MyApp {   static void Main()   {     Console.WriteLine("Hi from C#");   } } ' Hello World in VB. Imports System Public Module MyApp   Sub Main()     Console.WriteLine("Hi from VB")   End Sub End Module Notice that each language is using the Console class defined in the System namespace. Beyond some obvious syntactic variations, these applications look and feel very much alike, both physically and logically. Clearly, once you are comfortable with your.NET programming language of choice, your next goal as a.NET developer is to get to know the wealth of types defined in the (numerous).NET namespaces. The most fundamental namespace to get your head around initially is named System. This namespace provides a core body of types that you will need to leverage time and again as a.NET developer. In fact, you cannot build any sort of functional C# application without at least making a reference to the System namespace, as the core data types (e.g., System.Int32, System.String) are defined here. Table 1-3 offers a rundown of some (but certainly not all) of the.NET namespaces grouped by related functionality. 20 Chapter 1 Introducing C# and.NET 6 Table 1-3. A Sampling of.NET Namespaces.NET Namespace Meaning in Life System Within System, you find numerous useful types dealing with intrinsic data, mathematical computations, random number generation, environment variables, and garbage collection, as well as a number of commonly used exceptions and attributes. System.Collections These namespaces define a number of stock container types, as System.Collections.Generic well as base types and interfaces that allow you to build customized collections. System.Data These namespaces are used for interacting with relational databases System.Data.Common using ADO.NET. System.Data.SqlClient System.IO These namespaces define numerous types used to work with file I/O, System.IO.Compression compression of data, and port manipulation. System.IO.Ports System.Reflection These namespaces define types that support runtime type discovery as System.Reflection.Emit well as dynamic creation of types. System.Runtime. This namespace provides facilities to allow.NET types to interact with InteropServices unmanaged code (e.g., C-based DLLs and COM servers) and vice versa. System.Drawing These namespaces define types used to build desktop applications System.Windows.Forms using.NET’s original UI toolkit (Windows Forms). System.Windows The System.Windows namespace is the root for several namespaces System.Windows.Controls that are used in Windows Presentation Foundation applications. System.Windows.Shapes System.Windows.Forms The System.Windows.Forms namespace is the root for several System.Drawing namespaces used in Windows Forms applications. System.Linq These namespaces define types used when programming against the System.Linq.Expressions LINQ API. System.AspNetCore This is one of many namespaces that allows you to build ASP.NET Core web applications and RESTful services. System.Threading These namespaces define numerous types to build multithreaded System.Threading.Tasks applications that can distribute workloads across multiple CPUs. System.Security Security is an integrated aspect of the.NET universe. In the security-centric namespaces, you find numerous types dealing with permissions, cryptography, etc. System.Xml The XML-centric namespaces contain numerous types used to interact with XML data. 21 Chapter 1 Introducing C# and.NET 6 Accessing a Namespace Programmatically It is worth reiterating that a namespace is nothing more than a convenient way for us mere humans to logically understand and organize related types. Consider again the System namespace. From your perspective, you can assume that System.Console represents a class named Console that is contained within a namespace called System. However, in the eyes of the.NET runtime, this is not so. The runtime engine sees only a single class named System.Console. In C#, the using keyword simplifies the process of referencing types defined in a particular namespace. Here is how it works. Returning to the Calc example program earlier in this chapter, there is a single using statement at the top of the file. using System; That statement is a shortcut to enable this line of code: Console.WriteLine("10 + 84 is {0}.", ans); Without the using statement, the code would need to be written like this: System.Console.WriteLine("10 + 84 is {0}.", ans); While defining a type using the fully qualified name provides greater readability, I think you’d agree that the C# using keyword reduces keystrokes. In this text, we will avoid the use of fully qualified names (unless there is a definite ambiguity to be resolved) and opt for the simplified approach of the C# using keyword. However, always remember that the using keyword is simply a shorthand notation for specifying a type’s fully qualified name, and either approach results in the same underlying CIL (given that CIL code always uses fully qualified names) and has no effect on performance or the size of the assembly. Global Using Statements (New 10.0) As you build more complex C# applications, you will most likely have namespaces repeated in multiple files. Introduced in C# 10, namespaces can be referenced globally, and then be available in every file in the project automatically. Simply add the global keyword in front of your using statements, like this: global using System; Note All global using statements must come before any non-global using statements. A recommendation is that you place the global using statements along with your top level statements (covered in Chapter 3) or a completely separate file (such as GlobalUsings.cs) for better visibility. You will see many examples of this throughout this text. In addition to placing the global using statements in Program.cs (or a separate file), they can be placed in the project file for the application using the following format:             22 Chapter 1 Introducing C# and.NET 6 Implicit Global Using Statements (New 10.0) Another new feature included with.NET 6/C# 10 are implicit global using statements. The implicit global using statements supplied by.NET 6 varies based on the type of application you are building. Table 1-4 lists the types of applications and the included namespaces. Table 1-4. A Sampling of.NET Namespaces.NET Application Type Namespaces covered by implicit global using statements Client (Microsoft.NET.Sdk) System System.Collections.Generic System.IO System.Linq System.Net.Http System.Threading System.Threading.Tasks Web (Microsoft.NET.Sdk.Web) All from Microsoft.NET.Sdk plus: System.Net.Http.Json Microsoft.AspNetCore.Builder Microsoft.AspNetCore.Hosting Microsoft.AspNetCore.Http Microsoft.AspNetCore.Routing Microsoft.Extensions.Configuration Microsoft.Extensions.DependencyInjection Microsoft.Extensions.Hosting Microsoft.Extensions.Logging Worker Service (Microsoft.NET.Sdk.Worker) All from Microsoft.NET.Sdk plus: Microsoft.Extensions.Configuration Microsoft.Extensions.DependencyInjection Microsoft.Extensions.Hosting Microsoft.Extensions.Logging The vast majority of the C# 10 project templates enable global implicit using statements by default with the ImplicitUsings element in the project’s main Property group. To disable the setting, update the project file to the following:   net6.0   enable   disable To see the global using statements in your project, look for the.GlobalUsings.g.cs file in the \obj\Debug\net6.0 folder. For the Calc.cs project, the following is the generated code: // global using global::System; global using global::System.Collections.Generic; global using global::System.IO; global using global::System.Linq; 23 Chapter 1 Introducing C# and.NET 6 global using global::System.Net.Http; global using global::System.Threading; global using global::System.Threading.Tasks; File Scoped Namespaces (New 10.0) Also new in C# 10, file-scoped namespaces remove the need to wrap your code in braces when placing it in a custom namespace. Take the following example of the Calculator class, contained in the CalculatorExamples namespace. Prior to C# 10, to place a class in a namespace required the namespace declaration, an opening curly brace, the code (Calculator), and then a closing curly brace. In the example, the extra code is in bold: namespace CalculatorExamples {   class Calculator()   {   ...   } } As your code becomes more complex, this can add a lot of extra code and indentation. With file scoped namespaces, the following code achieves the same effect: namespace CalculatorExamples class Calculator() {... } Note Custom namespaces are covered in depth in Chapter 16 Referencing External Assemblies Prior versions of the.NET Framework used a common installation location for framework libraries known as the Global Assembly Cache (GAC). Instead of having a single installation location,.NET does not use the GAC. Instead, each version (including minor releases) is installed in its own location (by version) on the computer. When using Windows, each version of the runtime and SDK gets installed into c:\Program Files\dotnet. Adding assemblies into most.NET projects is done by adding NuGet packages (covered later in this text). However,.NET applications targeting (and being developed on) Windows still have access to COM libraries. For an assembly to have access to another assembly that you are building (or someone built for you), you need to add a reference from your assembly to the other assembly and have physical access to the other assembly. Depending on the development tool you are using to build your.NET applications, you will have various ways to inform the compiler which assemblies you want to include during the compilation cycle. 24

Use Quizgecko on...
Browser
Browser