dotnet 职业技术学院

博客

dotnet 职业技术学院

WPF's multi-threaded UI is not thread safe

dotnet 职业技术学院 发布于 2019-01-29

WPF supports multiple UI threads in its framework. You can create multiple UI thread windows or create multiple UI threads in a single window. But unfortunately, this is not really thread-safe.

There is a very low probability that WPF application will crash when you creating a multi-thread UI. In this post, I’ll tell how this happens.


The Issue

Necessary conditions:

  1. Create multiple WPF UI threads
    • In fact, two are enough, one is the main UI thread with the App class we usually write; a background UI thread, for example, to display the UI thread that starts the splash screen.
    • If you use two threads, you need a lot of repetitive trials to reproduce; and by creating more threads you can greatly improve the probability of a single recurrence
  2. These UI threads all display WPF windows
  3. This issue will occur in both WPF on .NET Core 3 and WPF on .NET Framework 4.7.2.

phenomenon:

 - An exception is thrown and the application crashes

For example, the following is one of the exceptions:

Exception thrown: 'System.NullReferenceException' in WindowsBase.dll
Object reference not set to an instance of an object.

System.NullReferenceException: Object reference not set to an instance of an object.
   at System.IO.Packaging.PackagePart.CleanUpRequestedStreamsList()
   at System.IO.Packaging.PackagePart.GetStream(FileMode mode, FileAccess access)
   at System.Windows.Application.LoadComponent(Object component, Uri resourceLocator)
   at Walterlv.Bugs.MultiThreadedUI.SplashWindow.InitializeComponent() in C:\Users\lvyi\Desktop\Walterlv.Bugs.MultiThreadedUI\Walterlv.Bugs.MultiThreadedUI\SplashWindow.xaml:line 1
   at Walterlv.Bugs.MultiThreadedUI.SplashWindow..ctor() in C:\Users\lvyi\Desktop\Walterlv.Bugs.MultiThreadedUI\Walterlv.Bugs.MultiThreadedUI\SplashWindow.xaml.cs:line 24
   at Walterlv.Bugs.MultiThreadedUI.Program.<>c__DisplayClass1_0.<RunSplashWindow>b__0() in C:\Users\lvyi\Desktop\Walterlv.Bugs.MultiThreadedUI\Walterlv.Bugs.MultiThreadedUI\Program.cs:line 33

The following image is an exception caught in WPF on .NET Core 3 that is shown in visual studio 2019:

The exception

How to Reproduce

  1. Create a new WPF project (either .NET Core 3 or .NET Framework 4.7.2)
  2. Keep the automatically generated App and MainWindow unchanged, we create a new window SplashWindow.
  3. Create a new Program class containing the Main function and set Program as the startup object (instead of App) in the project properties.

The project structure

All other files remain the same as the default code generated by Visual Studio, and the code of Program.cs is as follows:

using System;
using System.Threading;
using System.Windows.Threading;

namespace Walterlv.Bugs.MultiThreadedUI
{
    public class Program
    {
        [STAThread]
        private static void Main(string[] args)
        {
            for (var i = 0; i < 50; i++)
            {
                RunSplashWindow(i);
            }

            var app = new App();
            app.InitializeComponent();
            app.Run();
        }

        private static void RunSplashWindow(int index)
        {
            var thread = new Thread(() =>
            {
                var window = new SplashWindow
                {
                    Title = $"SplashWindow {index.ToString().PadLeft(2, ' ')}",
                };
                window.Show();
                Dispatcher.Run();
            })
            {
                IsBackground = true,
            };
            thread.SetApartmentState(ApartmentState.STA);
            thread.Start();
        }
    }
}

Remarks: Even if you add this code just before the Splash Window creating, this exception still occurs.

SynchronizationContext.SetSynchronizationContext(
    new DispatcherSynchronizationContext(
        Dispatcher.CurrentDispatcher));

WPF 支持的多线程 UI 并不是线程安全的

dotnet 职业技术学院 发布于 2019-01-29

WPF 支持创建多个 UI 线程,跨窗口的或者窗口内的都是可以的;但是这个过程并不是线程安全的。

你有极低的概率会遇到 WPF 多线程 UI 的线程安全问题,说直接点就是崩溃。本文将讲述其线程安全问题。


简述这个线程安全问题

必要条件:

  1. 创建多个 WPF UI 线程
    • 其实两个就够了,一个我们平时写的 App 类所在的主 UI 线程;一个后台 UI 线程,例如用来显示启动闪屏的 UI 线程
    • 两个线程的话你需要大量重复试验才能复现;而创建更多线程可以大大提高单次复现概率
  2. 这些 UI 线程都显示 WPF 窗口
  3. 无论是 .NET Framework 4.7.2 版本的 WPF,还是 .NET Core 3 版本的 WPF 都会出现此问题

现象:

  • 抛出异常,程序崩溃

比如下面是其中一种异常:

Exception thrown: 'System.NullReferenceException' in WindowsBase.dll
Object reference not set to an instance of an object.

System.NullReferenceException: Object reference not set to an instance of an object.
   at System.IO.Packaging.PackagePart.CleanUpRequestedStreamsList()
   at System.IO.Packaging.PackagePart.GetStream(FileMode mode, FileAccess access)
   at System.Windows.Application.LoadComponent(Object component, Uri resourceLocator)
   at Walterlv.Bugs.MultiThreadedUI.SplashWindow.InitializeComponent() in C:\Users\lvyi\Desktop\Walterlv.Bugs.MultiThreadedUI\Walterlv.Bugs.MultiThreadedUI\SplashWindow.xaml:line 1
   at Walterlv.Bugs.MultiThreadedUI.SplashWindow..ctor() in C:\Users\lvyi\Desktop\Walterlv.Bugs.MultiThreadedUI\Walterlv.Bugs.MultiThreadedUI\SplashWindow.xaml.cs:line 24
   at Walterlv.Bugs.MultiThreadedUI.Program.<>c__DisplayClass1_0.<RunSplashWindow>b__0() in C:\Users\lvyi\Desktop\Walterlv.Bugs.MultiThreadedUI\Walterlv.Bugs.MultiThreadedUI\Program.cs:line 33

下图是 .NET Core 3 版本的 WPF 中在 Visual Studio 2019 抓到的异常:

异常

复现步骤

  1. 创建一个新的 WPF 项目(无论是 .NET Framework 4.7.2 还是 .NET Core 3)
  2. 保持自动生成的 AppMainWindow 不变,我们额外创建一个窗口 SplashWindow
  3. 创建一个新的包含 Main 函数的 Program 类,并在项目属性中设置 Program 为启动对象(替代 App)。

项目结构

其他文件全部保持 Visual Studio 生成的默认代码不变,而 Program.cs 的代码如下:

using System;
using System.Threading;
using System.Windows.Threading;

namespace Walterlv.Bugs.MultiThreadedUI
{
    public class Program
    {
        [STAThread]
        private static void Main(string[] args)
        {
            for (var i = 0; i < 50; i++)
            {
                RunSplashWindow(i);
            }

            var app = new App();
            app.InitializeComponent();
            app.Run();
        }

        private static void RunSplashWindow(int index)
        {
            var thread = new Thread(() =>
            {
                var window = new SplashWindow
                {
                    Title = $"SplashWindow {index.ToString().PadLeft(2, ' ')}",
                };
                window.Show();
                Dispatcher.Run();
            })
            {
                IsBackground = true,
            };
            thread.SetApartmentState(ApartmentState.STA);
            thread.Start();
        }
    }
}

说明:即便在 new SplashWindow 代码之前调用以下方法修改 SynchronizationContext 也依然会发生异常。

SynchronizationContext.SetSynchronizationContext(
    new DispatcherSynchronizationContext(
        Dispatcher.CurrentDispatcher));

使用 Xamarin 在 iOS 真机上部署应用进行调试

dotnet 职业技术学院 发布于 2019-01-27

虽然 Xamarin 可以在 Windows 操作系统上编写和调试,但如果开发 iOS 应用,那么我们依然需要一台安装有 XCode 和 Visual Studio for Mac 的 Mac 电脑。做真机部署不是像平时使用太阳系第一 IDE Visual Studio 那样方便。

所以本文需要介绍如何使用 Xamarin 在 iOS 真机上部署应用进行调试,然后顺便说一些注意事项。


准备一台 Mac 电脑

如果你没有 Mac 电脑,那我只能很不幸地告诉你:本文读下去已经没有什么用了,你不会成功的……当然你也可以考虑使用 Mac OS 虚拟机,但成功率太低,本文不会涉及。

在 Mac 电脑上安装以下两款必备应用:

  1. XCode:从苹果应用商店安装
  2. Visual Studio for Mac:在这里下载 https://visualstudio.microsoft.com/vs/mac/

这两款应用的体积都很大,如果你没有很好的网络代理设置,安装一整天都是可能的。所以还是强烈建议你有一个稳定的代理网络来下载。

本文接下来的内容都假设你已经安装好了这两款应用。

背景知识

你需要知道一些背景知识,不然后面真机部署的时候失败了都不知道怎么回事。

  1. 你的账号必须是苹果开发者账号
  2. 只有 XCode 才能生成开发者的 provisioning profiles
  3. 只有 XCode 才能在 iOS 真机上部署全新的应用

也就是说,你必须有一些操作是在 XCode 中完成;只使用 Visual Studio for Mac 是无法完成部署任务的。

在 XCode 中准备

  1. 在 XCode 中新建一个空白 iOS 项目(什么类型都可以),这个项目随时可以丢弃。
  2. 选择你新建的项目,会出现这个项目的信息可以填,默认在 General 标签中。
  3. *[重要] 修改 Bundle Identifier。
    • 将这个 Bundle Identifier 修改为你希望部署的应用的 Bundle Identifier。比如你在 Xamarin 的 Info.plist 中写的 Bundle Identifier 是 com.walterlv.CloudKeyboard,那么这里也必须写 com.walterlv.CloudKeyboard
  4. *[重要] 一定要让这个 Bundle Identifier 文本框失焦(比如按下 Tab 或在其他文本框中点一下)。
    • 这个时候下面的 Signing Certificate 会出现一个加载中的动画,大概持续不到一秒钟,就会生成 iPhone Developer 的信息,这个就是包含 provisioning profiles 的信息(可以在 Provisioning Profile 旁边的感叹号中看到详细信息)
  5. 在 Mac 上插入你的 iPhone,解锁 iPhone,等待左上角出现你 iPhone 的名称和图标。
  6. 点击 XCode 左上角的运行按钮,等待这个空白的应用部署到你的手机上。

在 XCode 中进行设置

*[重要] 额外的,如果你开发的是 iOS 扩展,有两个或者更多的包,那么你需要重复步骤 3 到 6。也就是不断地修改 Bundle Identifier,等待生成新的 Developer 信息,然后部署这个空的应用

在 Visual Studio for Mac 中部署

  1. *[重要] 请回到你的 iPhone 手机,删除刚刚部署的应用
    • 如果你刚刚部署了多个空白应用,那么都要删除
  2. 回到 Visual Studio for Mac 并打开你的 Xamarin 项目,然后打开准备部署的应用的 Info.plist 文件
  3. 检查 Bundle Identifier,一定要确认跟前面 XCode 中填入的是同一个 Bundle Identifier
    • 额外的,如果你是开发 iOS 扩展,有两个或更多包,那么每个包都需要进入 Info.plist 文件检查 Bundle Identifier
  4. 点击 Bundle Signing Options,选择刚刚使用 XCode 生成的开发者信息(如果你看不到,那么就是前面 XCode 的步骤没有执行正确)
  5. 在 Mac 上插入你的 iPhone,解锁 iPhone,等待左上角出现你 iPhone 的名称和图标。
    • 如果没有出现,你可能需要点击一下 Debug iPhone 区域,一定要确保选中了 iPhone 而不是 iPhone Simulator
  6. 点击 Visual Studio for Mac 左上角的运行按钮,等待你 Xamarin 的应用部署到你的手机上(可能需要数十秒到数分钟)。

检查 Bundle Identifier

设置 Bundle Signing Options

运行与部署

理论上经过以上步骤,你就可以在你的 iPhone 上看到你用 Xamarin 开发的应用了。但其实是无法运行的。

如果部署过程中发生了任何错误,请:

  1. 检查你的步骤与本文是否有出入;
  2. 参考:使用 Xamarin 开发 iOS 应用中需要注意的若干个问题

在 iPhone 上操作

  1. 打开设置 -> 通用 -> 设备管理
  2. 点开 [自己的开发者账号],点击 [信任]

如果你是首次进行此操作(实际上阅读本文操作的应该也就是首次了),那么信任自己的开发者账号可能会花比较长的时间,Visual Studio for Mac 的部署调试可能会因为等待超时而调试失败。不过这不重要,你只需要在 Visual Studio for Mac 上点击停止调试,然后再次重来就可以了。

还需要注意,如果你删除了你部署的应用,那么下次部署的时候在 iPhone 上的操作部分需要重新进行。

还需要注意,可能每过 6 天,本文所述的所有步骤都需要重新进行一遍。

.NET/C# 编译期间能确定的相同字符串,在运行期间是相同的实例

dotnet 职业技术学院 发布于 2019-01-25

我们知道,在编译期间相同的字符串,在运行期间就会是相同的字符串实例。然而,如果编译期间存在字符串的运算,那么在运行期间是否是同一个实例呢?

只要编译期间能够完全确定的字符串,就会是同一个实例。


字符串在编译期间能确定的运算包括:

  1. A + B 即字符串的拼接
  2. $"{A}" 即字符串的内插

字符串拼接

对于拼接,我们不需要运行便能知道是否是同一个实例:

private const string X = "walterlv is a";
private const string Y = "逗比";
private const string Z = X + Y;

以上这段代码是可以编译通过的,因为能够写为 const 的字符串,一定是编译期间能够确定的。

字符串内插

对于字符串内插,以上代码我们不能写成 const

错误提示

错误提示为:常量的初始化必须使用编译期间能够确定的常量。

然而,这段代码不能在编译期间确定吗?实际上我们有理由认为编译器其实是能够确定的,只是编译器这个阶段没有这么去做而已。

实际上在 2017 年就有人在 GitHub 上提出了这个问题,你可以在这里看讨论:

但是,我们写一个程序来验证这是否是同一个实例:

using System;

namespace Walterlv.Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(ReferenceEquals(A, A));
            Console.WriteLine(ReferenceEquals(C, C));
            Console.WriteLine(ReferenceEquals(E, E));
            Console.WriteLine(ReferenceEquals(G, G));
            Console.ReadKey(true);
        }

        private static string A => $"walterlv is a {B}";
        private static string B => "逗比";
        private static string C => $"walterlv is a {D}";
        private static string D = "逗比";
        private static string E => $"walterlv is a {F}";
        private static readonly string F = "逗比";
        private static string G => $"walterlv is a {H}";
        private const string H = "逗比";
    }
}

以上代码的输出为:

False
False
False
True

也就是说,对于最后一种情况,也就是内插的字符串是常量的时候,得到的字符串是同一个实例;这能间接证明编译期间完全确定了字符串 G。

注意,其他情况都不能完全确定:

  1. 属性内插时一定不确定;
  2. 静态字段内插时,无论是否是只读的,都不能确定。(谁知道有没有人去反射改掉呢?)

我们可以通过 IL 来确定前面的间接证明(代码太长,我只贴出来最重要的 G 字符串,以及一个用来比较的 E 字符串):

.method private hidebysig static specialname string
    get_G() cil managed
{
    .maxstack 8

    // [22 36 - 22 56]
    IL_0000: ldstr        "walterlv is a 逗比"
    IL_0005: ret

}
.method private hidebysig static specialname string
    get_E() cil managed
{
    .maxstack 8

    // [20 36 - 20 56]
    IL_0000: ldstr        "walterlv is a "
    IL_0005: ldsfld       string Walterlv.Demo.Roslyn.Program::F
    IL_000a: call         string [System.Runtime]System.String::Concat(string, string)
    IL_000f: ret

}

可以发现,实际上 G 已经在编译期间完全确定了。

扩展:修改编译期间的字符串

前面我们说到可以在编译期间完全确定的字符串。呃,为什么一定要抬杠额外写一节呢?

下面我们修改编译期间确定的字符串,看看会发生什么:

static unsafe void Main(string[] args)
{
    // 这里的 G 就是前面定义的那个 G。
    Console.WriteLine("walterlv is a 逗比");
    Console.WriteLine(G);
    fixed (char* ptr = "walterlv is a 逗比")
    {
        *ptr = 'W';
    }
    Console.WriteLine("walterlv is a 逗比");
    Console.WriteLine(G);

    Console.ReadKey(true);
}

运行结果是:

walterlv is a 逗比
walterlv is a 逗比
Walterlv is a 逗比
Walterlv is a 逗比

虽然我们看起来只是在修改我们自己局部定义的一个字符串,但是实际上已经修改了另一个常量以及属性 G。

少年,使用指针修改字符串是很危险的!鬼知道你会把程序改成什么样!


参考资料

使用 Xamarin 开发 iOS 应用中需要注意的若干个问题

dotnet 职业技术学院 发布于 2019-01-22

本文收集整理使用 Xamarin 开发 iOS 应用时可能会遇到的各种问题。


需要注册 Apple Developer Portal

不管你用什么开发 iOS 应用,成为一个 Apple 的开发者是必要的。

  1. 访问:https://developer.apple.com/register/
  2. 登录
  3. 同意协议

完成!虽然简单,但是如果没有成为开发者,那么你在所有工具上都无法成功部署应用。

Could not find any available provisioning profiles for iOS

这个错误可能出现在你是用 Visual Studio 或者 Visual Studio for Mac 部署真机调试的时候出现。

只有 XCode 才能生成 provisioning profiles!所以,如果你希望只使用 Visual Studio 或者 Visual Studio For Mac 或者 Xamarin 来部署是不可能的。

如果出现了此错误,你需要使用 XCode 提前生成一份 provisioning profiles 然后在 Visual Studio 中使用这份 profiles。

方法:

  1. 在 XCode 中新建一个项目;
  2. 填写 Bundle Identifier:
    • 注意:必须写成跟你待会儿用 Visual Studio 部署时项目一模一样的 Bundle Identifier!
    • 比如你在 Visual Studio for Mac 中准备部署的应用为 com.walterlv.CloudKeyboard,那么在这里也必须填写 com.walterlv.CloudKeyboard
  3. 在 XCode 中部署这个临时的项目;
    • 你必须确保真的成功部署到真机上了。
  4. 换回 Visual Studio,理论上你现在就可以成功部署了。

至于那个在 XCode 中临时建的项目,你可以丢掉,也可以留着。毕竟这种方式创建的 provisioning profiles 只有 6 天的有效期。如果过期了,你就需要再来一次。

如果依然不能部署,你需要去项目中设置一下,Visual Studio 中的设置方法如下图:

设置 Provisioning

Visual Studio for Mac 中的设置方法则是选中这个项目的 Info.plist 文件,然后点击 Bundle Signing,在对话框中选。

需要注册 Apple Developer Program

注意,注册 Apple Developer Program 需要付 $99 美元的年费。

即便没有注册,也可以部署真机调试,但如上文所说,只有 6 天的有效期。如果注册了,那么有一年。


参考资料

C# 永远不会返回的方法真的不会返回

dotnet 职业技术学院 发布于 2019-01-20

一般情况下,如果一个方法声明了返回值,但是实际上在编写代码的时候没有返回,那么这个时候会出现编译错误。

然而,如果方法内部出现了永远也不会退出的死循环,那么这个时候就不会出现编译错误。


请看下面这一段代码,RunAndNeverReturns 方法声明了返回值 int 但实际上方法内部没有返回。这段代码是可以编译通过而且可以正常运行的。

namespace Walterlv.Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            RunAndNeverReturns();
        }

        private static int RunAndNeverReturns()
        {
            while (true)
            {
                Thread.Sleep(1000);
                Console.WriteLine("Walterlv will always appear.");
            }

            // 注意看,这个方法其实没有返回。
        }
    }
}

如果观察其 IL 代码,会发现此方法的 IL 代码里面是没有 ret 语句的。而其他正常的方法,即便返回值是 void,也是有 ret 语句的。

CentOS 的终端中如何搜索文件

dotnet 职业技术学院 发布于 2019-01-20

CentOS 中搜索文件可以使用 find 命令。


如果需要在当前文件夹中搜索文件,那么可以使用命令:

~$ find -name filename

其中 filename 是你需要找的文件或文件夹的名称。我们没有指定搜索文件的路径,默认是当前文件夹。

如果你希望在所有文件夹中查找,那么可以使用命令:

~$ find / -name filename

这里的 / 是根目录的意思,当然,你也可以指定为其他路径。

比如我要搜索 dotnet 的 SDK,可以使用:

~$ find / -name dotnet
/usr/share/dotnet
/usr/share/dotnet/dotnet

返回了两个 dotnet 文件夹。

也可以使用通配符:

~$ find / -name *.cs

参考资料

不使用 U 盘等任何工具全新安装 Windows 操作系统

dotnet 职业技术学院 发布于 2019-01-17

安装 Windows 有非常多种方法,现在我们要解决的问题是:

  1. 手头没有量产的 U 盘,或者懒得花时间去用 iso 文件量产 U 盘;
  2. 不想在 Windows 现有系统下安装(可能是为了全新安装,也可能是为了跳过安装序列号/产品密钥)

于是本文教你如何一步一步在 Windows RE 环境下安装操作系统。


准备工作

  1. Windows 10 的安装文件
    • 例如 cn_windows_10_consumer_editions_version_1809_updated_jan_2019_x64_dvd_34b4d4fb.iso
  2. 现有系统是 Windows 8/8.1/10 操作系统

第一步:解压 iso 文件

将 iso 文件解压到一个文件夹中,例如,我解压到 D:\Windows10 文件夹中。

解压 iso 到一个文件夹中

第二步:重启进入 RE 环境

现在,在开始菜单中点击电源按钮,这时会弹出电源选择菜单。注意:请按住 Shift 键不放,然后点击重启按钮,重启按钮点完之后才能松开 Shift 键。

按住 Shift 键点击重启按钮

第三步:等待进入 RE 环境

这时重启会进入 RE 环境。Windows RE 指的是 Windows Recovery Environment,也就是 Windows 恢复环境。你可以在这里进行很多系统之外的操作。相比于 PE 需要一个光盘或者 U 盘来承载,RE 是直接在你安装 Windows 8/8.1/10 时直接自带到机器硬盘上的。

进入 RE 环境

第四步:进入 RE 环境的命令提示符

依次进入 疑难解答 -> 高级选项 -> 命令提示符 -> 选择自己的账号 -> 输入自己的密码

注意,在选择命令提示符之后,计算机还会再重启一次,所以需要等一会儿才会到选择账号的界面。

疑难解答

高级选项

命令提示符

选择自己的账号

输入自己账号的密码

第五步:在命令提示符中找到安装程序

我们一开始将系统解压到了 D:\Windows10 文件夹下。一般来说,现在也应该是在 D 盘的 Windows10 文件夹下。不过有时候你会发现这里的 D 盘并不是你想象中那个 D 盘,你找不到那个文件夹和里面那个安装文件。这个时候可以去 C 盘、E 盘、F 盘等地方也看看。

命令提示符的操作这里就不赘述了,无非是 D: 跳转到某个盘符,cd 跳转到某个文件夹下,setup.exe 打开 setup.exe 这个程序。

打开 setup.exe

第六步:按照熟悉的安装系统的流程安装操作系统

现在,你应该可以看到熟悉的 Windows 10 安装界面了。

开始安装 Windows

比如,你可以在这里跳过产品密钥的输入:

跳过产品密钥的输入

选择 Windows 10 的安装版本

比如可以使用在 Windows 内部安装无法使用的“自定义”安装方式:

使用自定义的安装方式

甚至能在这里格式化所有分区,删除所有磁盘:

格式化分区或者删除磁盘

剩下的,祝你好运!

C#/.NET 如何确认一个路径是否是合法的文件路径

dotnet 职业技术学院 发布于 2019-01-16

很多方法要求传入一个字符串作为文件名或者文件路径,不过方法在实际执行到使用文件名的时候才会真正使用到这个文件名;于是这这种时候才会因为各种各样的异常发现文件名或者文件路径是不合法的。

有没有方法能够提前验证文件名或者文件路径是否是合法的路径呢?


这是一个不幸的结论 —— 没有!

实际上由我们自己写代码判断一个字符串是否是一个合法的文件路径是非常困难的,因为:

  1. 不同操作系统的路径格式是不同的;
  2. 同一个操作系统有各种各样不同的路径用途。

但你可能会说,就算有各种不同,也是可以穷举出来的。那么来看看穷举这些不同的情况需要多少代码吧:

看完这些代码,你是不是可以考虑放弃做 100% 精确的提前验证了?放弃是正解。

那么接下来如何验证呢?

使用 new FileInfo(string fileName) 类型和 Path.GetFullPath(string path) 方法来判断,则会使用到以上的代码,不过副作用是在路径不合法的时候抛出异常。

抛出异常

然而作为 API,验证路径的合法性也是需要抛出异常的,所以大可以继续使用这样的方法,用方法内部抛出的异常来提醒开发者传入的路径不合法。

但有时候是作为与用户的交互来判断路径或者文件名是否合法的,那么这个时候使用异常就不太合适了。毕竟 C#/.NET 的异常机制不应该参与正常的逻辑流程。

那么可以使用 Path.GetInvalidFileNameChars()GetInvalidPathChars() 来判断字符串中是否包含不合法的文件名字符或者路径字符。

以下代码来自 .NET Core 的库源码 Path.Windows.cs

public static char[] GetInvalidFileNameChars() => new char[]
{
    '\"', '<', '>', '|', '\0',
    (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
    (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
    (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
    (char)31, ':', '*', '?', '\\', '/'
};

public static char[] GetInvalidPathChars() => new char[]
{
    '|', '\0',
    (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
    (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
    (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
    (char)31
};

参考资料

让 MSBuild Target 支持 Clean

dotnet 职业技术学院 发布于 2019-01-16

我们有时候会使用解决方案的清理(Clean)功能来解决一些项目编译过程中非常诡异的问题。这通常是一些 Target 生成了一些错误的中间文件,但又不知道到底是哪里错了。

我们自己编写 Target 的时候,也可能会遇到这样的问题,所以让我们自己的 Target 也能支持 Clean 可以在遇到诡异问题的时候,用户可以自己通过清理解决方案来消除错误。


以下代码来自于 SourceFusion/Package.targets。这是我主导开发的一个预编译框架,用于在编译期间执行各种代码,以便优化代码的运行期性能。

<PropertyGroup>
    <CleanDependsOn>$(CleanDependsOn);_SourceFusionClean</CleanDependsOn>
</PropertyGroup>

  <!--清理 SourceFusion 计算所得的文件-->
<Target Name="_SourceFusionClean">
    <PropertyGroup>
        <_DefaultSourceFusionWorkingFolder Condition="'$(_DefaultSourceFusionWorkingFolder)' == ''">obj\$(Configuration)\</_DefaultSourceFusionWorkingFolder>
        <SourceFusionWorkingFolder Condition="'$(SourceFusionWorkingFolder)' == ''">$(_DefaultSourceFusionWorkingFolder)</SourceFusionWorkingFolder>
        <SourceFusionToolsFolder>$(SourceFusionWorkingFolder)SourceFusion.Tools\</SourceFusionToolsFolder>
        <SourceFusionGeneratedCodeFolder>$(SourceFusionWorkingFolder)SourceFusion.GeneratedCodes\</SourceFusionGeneratedCodeFolder>
    </PropertyGroup>
    <RemoveDir Directories="$(SourceFusionToolsFolder);$(SourceFusionGeneratedCodeFolder)" />
</Target>

这段代码的作用便是支持 Visual Studio 中的解决方案清理功能。通过指定 CleanDependsOn 属性的值给一个新的 Target,使得在 Clean 的时候,这个 Target 能够执行。我在 Target 中删除了我生成的所有中间文件。

你可以通过阅读 通过重写预定义的 Target 来扩展 MSBuild / Visual Studio 的编译过程 来了解这个 Target 是如何工作起来的。


参考资料

.NET 中 GetHashCode 的哈希值有多大概率会相同(哈希碰撞)

dotnet 职业技术学院 发布于 2019-01-10

如果你试图通过 GetHashCode 得到的一个哈希值来避免冲突,你可能要失望了。因为实际上 GetHashCode 得到的只是一个 Int32 的结果,而 Int32 只有 32 个 bit。

32 个 bit 的哈希,有多大概率是相同的呢?本文将计算其概率值。


对于 GetHashCode 得到的哈希值,

  1. 9292 个对象的哈希值冲突概率为 1%;
  2. 77163 个对象的哈希值冲突概率为 50%。

计算方法

计算哈希碰撞概率的问题可以简化为这样:

  1. 有 1, 2, 3, … \(n\) 这些数字;
  2. 现在,随机从这些数字中取出 \(k\) 个;
  3. 计算这 \(k\) 个数字里面出现重复数字的概率。

例如:

  1. 有 1, 2, 3, 4 这四个不同的数字;
  2. 现在从中随机抽取 2 个。

那么抽取出来的可能的情况总数为:

\[4^2\]

一定不会重复的可能的情况总数为:

\[4\times3\]

意思是,第一次抽取的时候有 4 个数字可以选,而第二次抽取的时候就只有 3 个数字可以选了。

那么,会出现重复的概率就是:

\[1-\frac{4\times3}{4^2}\]

也就是 25% 的概率会出现重复。

那么现在,我们随机抽取 3 个会怎样呢?

  1. 有 1, 2, 3, 4 这四个不同的数字;
  2. 现在从中随机抽取 3 个。

那么,会出现重复的概率就是:

\[1-\frac{4\times3\times2}{4^3}\]

也就是 37.5%,64 种可能里面,有 24 种是有重复的。

现在,我们推及到 GetHashCode 函数的重复情况。

GetHashCode 实际上返回的是一个 Int32 值,占 32 bit。也就是说,我们有 \(2^{32}\) 个数字可以选。

现在问题是:

  1. 有 1, 2, 3, … \(2^{32}\) 这些数字,我们把 \(2^{32}\) 记为 \(n\);
  2. 现在从中随机抽取 \(k\) 个。

那么会出现重复的概率为:

\[1-\frac{n\times(n-1)\times(n-2)\times...(n-k+1)}{n^k}\]

当然,分子分母都有的 \(n\) 可以约去:

\[1-\frac{(n-1)\times(n-2)\times...(n-k+1)}{n^{k-1}}\]

计算的简化

而 \(k\) 很大的时候,此概率的计算非常复杂。然而我们可以取近似值简化成如下形式 [1]

\[1-e^{\frac{-k(k-1)}{2n}}\]

当然,实际上此计算在 \(k\) 取值较小的时候还可以进一步简化成:

\[\frac{k(k-1)}{2n}\]

于是,在日常估算的时候,你甚至可以使用计算器估算出哈希值碰撞的概率。

你可以阅读 Hash Collision Probabilities 了解更多关于计算简化的内容。

概率图

为了直观感受到 32 bit 的哈希值的碰撞概率与对象数量之间的关系,我从 Socks, birthdays and hash collisionsHash Collision Probabilities 找到了计算好的概率数据,并绘制成一张图:

32 bit 的哈希值碰撞概率图


参考资料

int? 竟然真的可以是 null!.NET/C# 确定可空值类型 Nullable 实例的真实类型

dotnet 职业技术学院 发布于 2019-01-08

使用 Nullable<T> 我们可以为原本不可能为 null 的值类型像引用类型那样提供一个 null 值。不过注意:Nullable<T> 本身也是个 struct,是个值类型哦。这意味着你随时可以调用 .HasValue 这样的方法,而不用担心会出现 NullReferenceException

等等!除了本文提到的一些情况。


Nullable 中的 null

注意看以下的代码。我们创建了一个值为 nullint?,然后依次输出 value 的值、value.GetType()

你觉得可以得到什么结果呢?

public class Program
{
    public static void Main(string[] args)
    {
        int? value = GetValue(null);

        Console.WriteLine($"value = {value}");
        Console.WriteLine($"type  = {value.GetType()}");
        Console.WriteLine($"TYPE  = {typeof(int?)}");

        Console.ReadLine();
    }

    private static int? GetValue(int? source) => source;
}


结果是……


果是……


是……


……



崩掉了……

NullReferenceException

那么我们在 value 后面加个空传递运算符:

--  Console.WriteLine($"type  = {value.GetType()}");
++  Console.WriteLine($"type  = {value?.GetType()}");

现在再次运行,我们确认了 value?.GetType() 的值为 null;而 typeof(int?) 的类型为 Nullable<Int32>

null 的类型

然而,我们现在将 value 的值从 null 改为 1

--  int? value = GetValue(null);
++  int? value = GetValue(1);

竟然 value.GetType() 得到的类型是 Int32

1 的类型

于是我们可以得出结论:

  1. 对于可空值类型,当为 null 时,GetType() 会出现空引用异常;
  2. 对于可空值类型,当不为 null 时,GetType() 返回的是对应的基础类型,而不是可空值类型;
  3. typeof(int?) 能够得到可空值类型。

Object.GetType() 和 is 对 Nullable 的作用

docs.microsoft.com 中,有一段对此的描述:

When you call the Object.GetType method on an instance of a nullable type, the instance is boxed to Object. As boxing of a non-null instance of a nullable type is equivalent to boxing of a value of the underlying type, GetType returns a Type object that represents the underlying type of a nullable type.

意思是说,当你对一个可空值类型 Nullable<T> 调用 Object.GetType() 方法的时候,这个实例会被装箱,会被隐式转换为一个 object 对象。然而对可空值类型的装箱与对值类型本身的装箱是同样的操作,所以调用 GetType() 的时候都是返回这个对象对应的实际基础类型。例如对一个 int? 进行装箱和对 int 装箱得到的 object 对象是一样的,于是 GetType() 实际上是不能区分这两种情况的。

那什么样的装箱会使得两个不同的类型被装箱为同一个了呢?

另一篇文档描述了 Nullable<T> 装箱的过程:

  • If HasValue returns false, the null reference is produced.
  • If HasValue returns true, a value of the underlying value type T is boxed, not the instance of Nullable.
  • 如果 HasValue 返回 false,那么就装箱一个 null
  • 如果 HasValue 返回 true,那么就将 Nullable<T> 中的 T 进行装箱,而不是 Nullable<T> 的实例。

这才是为什么 GetType() 会得到以上结果的原因。

同样的,也不能使用 is 运算符来确定这个类型到底是不是可空值类型:

Console.WriteLine($"value is int  = {value is int}");
Console.WriteLine($"value is int? = {value is int?}");

最终得到两者都是 True

用 is 确定类型

应该如何判断可空值类型的真实类型

使用 Nullable.GetUnderlyingType(type) 方法,能够得到一个可空值类型中的基础类型,也就是得到 Nullable<T>T 的类型。如果得不到就返回 null

所以使用以下方法可以判断 type 的真实类型。

bool IsNullable(Type type) => Nullable.GetUnderlyingType(type) != null;

然而,这个 type 的实例怎么来呢?根据前面的示例代码,我们又不能调用 GetType() 方法。

实际上,这个 type 的实例就是拿不到,在运行时是不能确定的。我们只能在编译时确定,就像下面这样:

bool IsOfNullableType<T>(T _) => Nullable.GetUnderlyingType(typeof(T)) != null;

如果你是运行时拿到的可空值类型的实例,那么实际上此方法也是无能为力的。

public class Program
{
    public static void Main(string[] args)
    {
        Console.Title = "walterlv's demo";

        int? value = GetValue(1);
        object o = value;
        Console.WriteLine($"value is nullable? {IsOfNullableType(value)}");
        Console.WriteLine($"o     is nullable? {IsOfNullableType(o)}");

        Console.ReadLine();
    }

    private static int? GetValue(int? source) => source;

    static bool IsOfNullableType<T>(T _) => Nullable.GetUnderlyingType(typeof(T)) != null;
}

运行时是拿不到的


参考资料

C# 中委托实例的命名规则

dotnet 职业技术学院 发布于 2019-01-08

我们知道一个类中的属性应该用名词或名词性短语,方法用动词或动宾短语;但是委托的实例却似乎有一些游离。因为在 .NET 中委托代表的是一个动作,既可以把它看作是名词,也可以看作是动词。在用法上,既可以像属性和变量一样被各种传递,也可以像一个方法一样被调用。

那么委托实例的命名,应该遵循属性和变量的命名,还是遵循方法的命名呢?


委托的实例可以当作属性或者变量使用:

var action = () => Console.WriteLine("walterlv is a 逗比");

委托的实例也可以当作方法使用:

var action = () => Console.WriteLine("walterlv is a 逗比");
action();

于是委托的命名方式迁就名词还是动词呢?

在微软的官方文档 Naming Guidelines 中提到了 .NET 中约定的命名方式。对于委托的命名,实际上只在 Names of Type Members 中提到了,不过提及的实际上是事件型的委托,而不是一般的委托实例。然后,微软其他地方的官方文档中也没有单独提及委托的命名方式。

为了弄清楚第一方代码的命名规则,我去 https://source.dot.net/ 上找了一些使用了委托的代码,然后发现,对于 ActionFunc 系列委托的命名,有以下这些(部分名称只保留了后缀进行合并):

使用名词的:

  • action
  • function
  • callback
  • continuation
  • method
  • factory
  • valueFactory
  • creator
  • valueGetter
  • initializer
  • _target
  • attributeComputer
  • argumentsPromise
  • taskProvider

使用动词的:

  • getSource

使用缩略词的:

  • localInit

我把缩略词单独拿出来,是因为缩写了以下就看不出来这到底是缩自名词还是缩自动词。

基本上可以确定:

委托实例的命名是 —— 一个表示动作的名词


参考资料

三值 bool? 进行与或运算后的结果

dotnet 职业技术学院 发布于 2019-01-06

bool? 实际上是 Nullable<Boolean> 类型,可以当作三值的 bool 类型来使用。不过三值的布尔进行与或运算时的结果与二值有什么不同吗?


重载条件逻辑运算符“与”(&&)“或”(||)

在 [C# 重载条件逻辑运算符(&& 和   )](/post/overload-conditional-and-and-or-operators-in-csharp) 一文中我说明了如何重载条件逻辑运算符 &&||

这两个运算符不能直接重载,但可以通过重载 &| 运算符来间接完成。

对于 bool?,重载了这样两个运算符:

  • bool? operator &(bool? x, bool? y)
  • bool? operator |(bool? x, bool? y)

于是我们可以得到三值 bool? 的与或结果。

三值 bool? 的与或结果

x y x&y x|y
true true true true
true false false true
true null null true
false true false true
false false false false
false null false null
null true null true
null false false null
null null null null

参考资料

为什么我们不应该使用微信或者 QQ 作为团队协作的 IM 工具?

dotnet 职业技术学院 发布于 2019-01-06

如果你的团队没有觉得微信是低效的团队 IM 工具,那只有两种可能:

  1. 团队成员很少使用微信进行私人的生活和娱乐。
  2. 你就是一个低效的团队,而且还不自知。

微信,连接一切

微信,连接一切。除了家人、朋友、同学这些熟人关系,还有同事、客户、用户、企业号这些工作上的关系,还有各种各种办事小程序、各种资讯公众号、各种商店服务号、快递通知等生活和私人的快捷入口。

每个人都有微信

微信的一大目标是让所有人都能用,所以其 UX 体验必须照顾所有人——要尽一切可能简单。这里我不需要说张小龙是如何做到这一点的,但是他就是做到了。因为即便是家里常年不碰手机的老人们都能够使用微信发一些语音消息,抢一些红包,秀一秀朋友圈了。

曾经见面要电话联系方式,因为知道每个人都有电话,一定能要得到的。而现在见面会要微信,因为确认每个人都有微信的。所以使用微信来做任何事情的习惯会不断在人群中传播。在某个线下活动中见了一面加一个微信和一堆微信群,在某个工作事件中加一堆内部和外部的微信群。反正,所有人的所有活动都用微信连接着你。

微信,低成本的沟通方式

发短信不是一个能确保送达的方式,因为现在的短信功能基本上只有广告和验证码的功能,没有人会认真去查阅短信收件箱的。所以短信的成本从原来的套餐价格成本提升到了别人可能不看的成本。

为了能够打一个电话,你得差不多确认对方应该有空才行,自己也需要一个可以大声说话的场合。而且打过去对方不一定方便接听。这就是较高的接听成本。

邮件,就像写信一样。我们很重视写信,会尽可能说明要点,写明事情。于是写邮件这个过程就是一个高成本的过程。

而微信,你无需进行过多的思考,只要你想,打几个字就能发出去。而且你还不用担心对方是否有空,因为当他有空的时候也一定会看见。所以发送的成本低,确保别人收到的成本也低。所以所有种类的人都可以有事没事在微信里面找你一下。

不间断的交叉消息干扰

咚!技术大佬发话了……

咚!快递到了……

咚!新的缴费通知……

咚!你有一些新的资讯……

咚!最新的客户问题跟进如何了……

咚!今晚要不要出去聚餐……

每时每刻,微信不断有消息袭来。早上、上午、中午、下午、晚上、深夜……

一个眨眼的功夫,微信图标上就会多几个数字。

我们将一个群设置为免打扰,是因为这个群会说到一些有用的信息,只是有效信息密度太低。虽然可以设置群消息免打扰,但不能屏蔽。免打扰的群消息依然占据一个聊天会话的位置。甚至我想找某人聊一些事情的时候,前一秒正准备点击那个会话,后一秒就被其他群的新消息置顶改变了会话顺序,于是会点错。

知识工作者思考打断的恢复成本很高。在工作期间会收到各种社区、朋友和第三方服务等非工作消息,在非工作期间会收到各种工作类的消息。这种交叉干扰使得我们做任何一件事情都比以前更难专注。如果不去看消息,又不能知道哪些消息是重要紧急的,哪些消息是自己关心的,哪些消息是无关紧要可以随后再看的。

于是,这种手工的消息分类和过滤,会发生在一天的每一个时刻,深入到工作和生活的每一个角落。

不止是消息的交叉干扰

以上只是痛点问题,而实际上还有其他的效率问题:

微信是为移动端而优化的应用。而实际上一旦说到效率,PC 端是远胜于移动端的。PC 端的信息承载量大、而键盘和鼠标能够快速处理大量信息;这一点是单页的移动端应用和触摸操作远不能比的。

然而,微信的 PC 端每次登录需要使用手机扫码或者点击,每天一次就意味着不断在无效的操作上浪费时间。

另外,微信的移动端和 PC 端是不自动互通的,具体来说是消息不互通,如果先在移动端收到了一份文件,还需要折腾一阵才能在 PC 端打开使用。

别随便什么事儿都拉一个群

当然,我指的是工作上的事情。因为多数人更关心工作上的效率,而不是娱乐上的效率。

微信上的消息那么多,如果这件事情与对方的关系不大,你根本不能指望对方能够时刻关注着群里事件的最新动向。嗯,这个群在对方的微信里面,只是众多垃圾信息中的一个而已。

IM 不适合用来沟通事情的进展,只是用来沟通。所以经常总结进展,仅在必要某人的时候将某人加入,阅读总结的进展才是高效的。

好一些的 QQ/TIM

TIM 不像微信那样期望所有人用,所以可以在里面放上更多高级的功能。比如群的消息屏蔽,可以设置为完全屏蔽到一个群助手里面。比如群管理,可以有更多的管理方式。

使用 TIM 至少可以根据群的有效信息密度划分更多等级的提示级别,可以比微信更容易的筛选和过滤对自己有用的消息。

另外,QQ/TIM 的移动端和 PC 端的同步是实时的自动的。可以随时在 PC 端继续,而无需受限在移动端上。

不过,QQ/TIM 依然聚合了各种维度的信息,依然会存在各种信息的交叉干扰。

我们需要新的消息过滤和聚合方式

实际上,只要团队协作不使用 QQ 或者微信这种聚合所有交流的工具进行团队协作,就能够大大降低团队成员的信息过滤的成本。

在工作期间,只注重团队专用 IM 工具发来的消息,而对于 QQ/微信发来的消息可以延迟统一查看。如果这款工具做得更好,那么可以为团队 IM 的不同消息进行分级:

  • 紧急的工作任务,应该只在紧急情况使用,可以打断相关成员。
  • 协同工作所需的聊天,应该只打扰相关的协同方;其他人可以关注进展,但不应该被打扰。
  • 工作期间也可以吹水,但这些信息一定不能不断打断团队成员。
  • 还有其他的消息过滤级别。

于是,我们在手机上可以通过 App 的不同来进行消息分级,避免不同种类的消息交叉干扰。在 PC 上通过任务栏上不同的软件来避免消息的交叉干扰;甚至工作期间在 PC 上可以不启动非工作用的软件。

考虑使用工作专用的 IM 工具

TIM 相比于 QQ,提供了更适合于工作场合的 UI/UX。可以让我们更加专注于工作本身而不必被 QQ 的其他功能打扰。不过 TIM 和 QQ 的消息是完全互通的,这样就做不到消息的自动过滤,依然存在消息之间的交叉干扰。

企业微信与微信的消息不是直接互通的,而是作为企业外部人员来对待。钉钉是一款全新的 IM,所以消息自然不会互通。这就断开了企业内部的消息和其他消息的交叉干扰。当工作时,会只注重由企业微信或者钉钉发来的消息,微信或者 QQ 的消息就可以延迟查看;而离开工作时,可以延迟关注来自于企业微信或者钉钉发来的消息,更加关注朋友与技术社区,专注于情感的建立与个人的成长。

反正,无论你用什么,只要不是 QQ 或者微信,团队的 IM 效率就能得到提升。

考虑使用 Slack

可以试试 Slack,这也是一款以 IM 为主的团队协作工具。

不过,它还带了更多的扩展 API 可以使用,可以接入到团队正在使用的各种系统中来。帮助我们将大量的手工任务改造成自动化完成的任务。