Mybatis+Oracle搭配insert空值報錯之myBatis+mysql驅動+oracle驅動的源碼分析

来源:https://www.cnblogs.com/guoyansi19900907/archive/2020/04/14/12696023.html
-Advertisement-
Play Games

為了便於SEO搜索到,首先把報錯內容貼出來吧 不同版本的Oracle驅動會報不同的錯 1 <dependency> 2 <groupId>com.oracle</groupId> 3 <artifactId>ojdbc6</artifactId> 4 <version>1.0</version> 5 ...


為了便於SEO搜索到,首先把報錯內容貼出來吧 

不同版本的Oracle驅動會報不同的錯 

1 <dependency>
2     <groupId>com.oracle</groupId>
3     <artifactId>ojdbc6</artifactId>
4     <version>1.0</version>
5 </dependency>

 報錯如下:

Error updating database.  Cause: org.apache.ibatis.type.TypeException: Could not set parameters for mapping: ParameterMapping{property='name', mode=IN, javaType=class java.lang.String, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}. Cause: org.apache.ibatis.type.TypeException: Error setting null for parameter #1 with JdbcType OTHER . Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. Cause: java.sql.SQLException: 無效的列類型: 1111

<dependency>
    <groupId>com.oracle</groupId>
    <artifactId>ojdbc4</artifactId>
    <version>1.0</version>
</dependency>

報錯如下:

Error updating database.  Cause: org.apache.ibatis.type.TypeException: Could not set parameters for mapping: ParameterMapping{property='name', mode=IN, javaType=class java.lang.String, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}. Cause: org.apache.ibatis.type.TypeException: Error setting null for parameter #1 with JdbcType OTHER . Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. Cause: java.sql.SQLException: 無效的列類型

如果不想看下麵裹腳布版的源碼分析,直接看我這篇博客尋找解決辦法吧:MyBatis+Oracle在執行insert時空值報錯之從源碼尋找解決辦法

有異常那就一點一點的對著MyBatis調試追蹤吧。避免啰嗦,就用ojdbc6調試吧;因為ojbc6與mybatis的最新版本搭配更穩定。

至於為什麼不穩定可以看看我的這篇博客:MyBatis+Oracle時出現的錯誤: Method oracle/jdbc/driver/OracleResultSetImpl.isClosed()Z is abstract

便於源碼分析,還是先上Demo吧。

mybatis-oracle-config.xml 

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
 3         "http://mybatis.org/dtd/mybatis-3-config.dtd">
 4 
 5 <configuration>
 6     <properties>
 7         <property name="driver" value="oracle.jdbc.driver.OracleDriver"/>
 8         <property name="url" value="jdbc:oracle:thin:@127.0.0.1:1521/orcl"/>
 9     </properties>    
10 
11     <environments default="dev">
12         <environment id="dev">   
13             <dataSource type="POOLED">
14                 <property name="driver" value="${driver}"></property>
15                 <property name="url" value="${url}"></property>
16                 <property name="username" value="gys"></property>
17                 <property name="password" value="gys"></property>
18             </dataSource>
19         </environment>
20 
21     </environments>
22     <mappers>       
23         <mapper resource="mapper/oracle/user.xml"></mapper>
24     </mappers>
25 </configuration>

 user.xml

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 3         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 4 <mapper namespace="dao.oracle.IUserMapper">
 5     <insert id="insertUser" parameterType="model.oracle.User">
 6         insert into users
 7         (name,age)
 8         values
 9         (#{name},#{age})
10     </insert>
11 </mapper>

Main方法入口: 

 1  public static void main(String[] args) throws Exception{
 2         SqlSessionFactoryBuilder builder=new SqlSessionFactoryBuilder();
 3         SqlSessionFactory sqlSessionFactory=builder.build(Resources.getResourceAsStream("mybatis-oracle-config.xml"),"dev");
 4         SqlSession sqlSession=sqlSessionFactory.openSession(true);
 5         IUserMapper userMapper=sqlSession.getMapper(IUserMapper.class);
 6         User user=new User();
 7    //此處不設置,故意插入null數據
 8         //user.setName("gggg");
 9         user.setAge(20);
10         int count=userMapper.insertUser(user);
11         System.out.println(count == 1 ? "插入成功" : "插入失敗");        
12         sqlSession.close();
13     }

 運行結果就是上面的報錯內容了。

我們直接從SimpleExecutor.java執行器開始分析吧。

不瞭解執行器的可以看看我的這篇博客:MyBatis中Executor源碼解析之BatchExecutor搞不懂

 這個地方的stmt是指向OraclePreparedStatementWrapper.java這個類的;

看來這個是Oracle驅動提供的類,繼承了JDBC的Statement介面

同時這個handler是指向RoutingStatementHandler類

第88行代碼是開始進行sql參數進行設置的方法。我們追蹤進去看看是如何實現的。

直接去PreparedStatementHandler類吧;因為RoutingStatmentHandler繼承自PreparedStatmentHandler類。

 

 繼續看setParameters()源碼:

 1 @Override
 2 public void setParameters(PreparedStatement ps) {
 3 //獲取該sql中所有的參數映射對象
 4  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
 5   if (parameterMappings != null) {
 6    for (int i = 0; i < parameterMappings.size(); i++) {
 7       ParameterMapping parameterMapping = parameterMappings.get(i);    
 8       //如果不是出參
 9       if (parameterMapping.getMode() != ParameterMode.OUT) {
10        Object value;
11       //獲取參數的屬性名,比如name,age
12        String propertyName = parameterMapping.getProperty();       
13          MetaObject metaObject = configuration.newMetaObject(parameterObject);
14          //獲取參數的預設值,比如name=5,這裡value就是5
15          value = metaObject.getValue(propertyName);         
16          //根據參數獲取類型轉換器
17        TypeHandler typeHandler = parameterMapping.getTypeHandler();
18          //獲取jdbc類型,這裡是枚舉;如果是空著,返回other枚舉值,並且枚舉的code屬性值是1111
19        JdbcType jdbcType = parameterMapping.getJdbcType();
20        //這行條件基本不會執行,因為jdbcType在build時候,始終都會有值,空值的話預設是other枚舉
21        if (value == null && jdbcType == null) {
22          jdbcType = configuration.getJdbcTypeForNull();
23        }
24        //參數設置開始交給類型轉換器進行賦值
25        typeHandler.setParameter(ps, i + 1, value, jdbcType);
26      }
27    }
28  }
29 }   

上面代碼去除了干擾的代碼,添加了註釋,繼續向下追蹤

 

 typeHandler指向StringTypeHandler類,這裡面沒有seParameter()方法,直接去父級BaseTypeHandler類中找吧。

setParameter()源碼

下麵代碼去除多餘干擾的代碼

 1  @Override
 2   public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
 3       //參數值為空
 4     if (parameter == null) {
 5         //jdbcType為空,這裡不可能為空,最起碼是預設枚舉other
 6       if (jdbcType == null) {
 7         throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
 8       }
 9       try {
10           /**
11           i是參數位置,第一個參數這裡就是1
12         jdbcType.TYPE_CODE是枚舉的編碼值,這裡空值是1111·    
13           **/
14         ps.setNull(i, jdbcType.TYPE_CODE);
15       } catch (SQLException e) {
16           //這裡的異常內容是不是很熟悉,就是我們在控制台看到的內容。看來異常就是上面setNull方法拋出的了
17         throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "
18               + "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "
19               + "Cause: " + e, e);
20       }
21     }
    //如果不是空值,就直接走這裡了
    else{
      
setNonNullParameter(ps, i, parameter, jdbcType);

    }
22 }

我不明白為什麼要把jdbcType為空是,編碼設置成1111;這個值是有什麼特殊的含義麽?有知道的,麻煩告知一下

繼續查看setNull()方法

setNull()方法源碼

  繼續去T4CPreparedStatement中查看setNull()源碼

  繼續追蹤setNullCritical()源碼

 View Code

 繼續追蹤到getInternalType()源碼

獲取oracle內部的欄位類型

  1 int getInternalType(int var1) throws SQLException {
  2         boolean var2 = false;
  3         short var4;
  4         switch(var1) {
  5         case -104:
  6             var4 = 183;
  7             break;
  8         case -103:
  9             var4 = 182;
 10             break;
 11         case -102:
 12             var4 = 231;
 13             break;
 14         case -101:
 15             var4 = 181;
 16             break;
 17         case -100:
 18         case 93:
 19             var4 = 180;
 20             break;
 21         case -16:
 22         case -1:
 23             var4 = 8;
 24             break;
 25         case -15:
 26         case -9:
 27         case 12:
 28             var4 = 1;
 29             break;
 30         case -14:
 31             var4 = 998;
 32             break;
 33         case -13:
 34             var4 = 114;
 35             break;
 36         case -10:
 37             var4 = 102;
 38             break;
 39         case -8:
 40             var4 = 104;
 41             break;
 42         case -7:
 43         case -6:
 44         case -5:
 45         case 2:
 46         case 3:
 47         case 4:
 48         case 5:
 49         case 6:
 50         case 7:
 51         case 8:
 52             var4 = 6;
 53             break;
 54         case -4:
 55             var4 = 24;
 56             break;
 57         case -3:
 58         case -2:
 59             var4 = 23;
 60             break;
 61         case 0:
 62             var4 = 995;
 63             break;
 64         case 1:
 65             var4 = 96;
 66             break;
 67         case 70:
 68             var4 = 1;
 69             break;
 70         case 91:
 71         case 92:
 72             var4 = 12;
 73             break;
 74         case 100:
 75             var4 = 100;
 76             break;
 77         case 101:
 78             var4 = 101;
 79             break;
 80         case 999:
 81             var4 = 999;
 82             break;
 83         case 2002:
 84         case 2003:
 85         case 2007:
 86         case 2008:
 87         case 2009:
 88             var4 = 109;
 89             break;
 90         case 2004:
 91             var4 = 113;
 92             break;
 93         case 2005:
 94         case 2011:
 95             var4 = 112;
 96             break;
 97         case 2006:
 98             var4 = 111;
 99             break;
100         default:
101             SQLException var3 = DatabaseError.createSqlException(this.getConnectionDuringExceptionHandling(), 4, Integer.toString(var1));
102             var3.fillInStackTrace();
103             throw var3;
104         }
105 
106         return var4;
107     }

因為case中沒有1111匹配項,所以只能進入default中了。

default中定義了一個異常類,併在最後義無反顧的throw掉了。一個空值的賦值處理總算告一段落了。

這個地方不是太明白什麼意思,這些case 後面的數值都代表什麼意思,我看只有oracle驅動開發的人才能明白了。

這個地方的設計好奇怪啊;

上面setNullCritical()源碼中的case數值,大致可以推斷字元串類型空值的編號是1,8,96,995,那麼getInternalType()中的case數值推斷sqlType=70,-8,1,0;

等會下麵JDBC例子中,將剛纔我們推斷的sqlType值設置到空值裡面取,試試能否成功。

Mybatis+ojbc6對於傳入空值拋出的異常是:" Cause: java.sql.SQLException: 無效的列類型: 1111"

這裡的1111是Mybatis中對於不明確的jdbcType參數給出的編號。和oracle驅動是沒有半毛錢關係的。

到這位置從mybatis到ojdbc6驅動的源碼分析算是結束了。

 

那麼java能否向oracle中發送一條帶有未經賦值的sql語句呢?

Mybatis是對JDBC的封裝,我們踢掉Mybatis,直接用jdbc+Oracle驅動來驗證上面的觀點。

 1 public static void main(String[] args) throws Exception{     
 2     String sql="insert into users(name,age) values(?,?)";
 3     Class.forName("oracle.jdbc.driver.OracleDriver");
 4     Connection connection=DriverManager.getConnection("jdbc:oracle:thin:@127.0.0.1:1521/orcl","gys","gys");
 5     PreparedStatement ps=connection.prepareStatement(sql);
 6     ps.setInt(2,30);
 7     //這裡故意不對第一個參數進行設置
 8     //ps.setString(1,null);
 9     ParameterMetaData metaData=ps.getParameterMetaData();
10     System.out.println(metaData.getParameterCount());//列印參數個數
11     int count=ps.executeUpdate();
12     System.out.println(count == 1 ? "插入成功" : "插入失敗");
13     connection.close();
14 }

執行結果:

  jdbc也不能向oracle中插入一個未經賦值的sql語句;但是如果將第8行代碼註釋放開,又可以進行正確的操作了。

疑問來了,為什麼Mybatis+Oracle和JDBC+Oracle都沒有對參數賦值,為什麼出現的報錯內容不一樣?

因為Mybatis對空值做了判斷,如果為空了直接交給ojdbc6的預編譯對象的setNull()方法處理了;

異常是在參數處理階段拋出的異常,還沒有到資料庫執行的這一步;而JDBC是報錯是在資料庫執行sql的時候報錯的;屬於sql語法錯誤了。

我們可以把上面的JDBC代碼做一個修改,也會出現和Mybatis一樣的異常錯誤

 1 public static void main(String[] args) throws Exception{     
 2     String sql="insert into users(name,age) values(?,?)";
 3     Class.forName("oracle.jdbc.driver.OracleDriver");
 4     Connection connection=DriverManager.getConnection("jdbc:oracle:thin:@127.0.0.1:1521/orcl","gys","gys");
 5     PreparedStatement ps=connection.prepareStatement(sql);
 6     ps.setInt(2,30);
 7     //這裡故意不對第一個參數進行設置
 8     //ps.setString(1,null);
 9     ps.setNull(1,1111);
10     ParameterMetaData metaData=ps.getParameterMetaData();
11     System.out.println(metaData.getParameterCount());
12     int count=ps.executeUpdate();
13     System.out.println(count == 1 ? "插入成功" : "插入失敗");
14     connection.close();
15 }

運行結果:

這裡沒有上面那個“Cause: org.apache.ibatis.type.TypeException.......”之類的關鍵詞是因為ojdbc6拋出的異常被mybatis捕獲了,mybatis添加了一些自己的內容。

繼續修改上面JDBC中的源碼,測試一遍

 1  String sql="insert into users(name,age) values(?,?)";
 2         Class.forName("oracle.jdbc.driver.OracleDriver");
 3         Connection connection=DriverManager.getConnection("jdbc:oracle:thin:@127.0.0.1:1521/orcl","gys","gys");
 4         PreparedStatement ps=connection.prepareStatement(sql);
 5         ps.setInt(2,30);
 6         //ps.setString(1,null);
 7         ps.setNull(1,70);
 8         ParameterMetaData metaData=ps.getParameterMetaData();
 9         System.out.println("參數個數:"+metaData.getParameterCount());
10         int count=ps.executeUpdate();
11         System.out.println(count == 1 ? "插入成功" : "插入失敗");
12         connection.close();

 

能夠正確的插入數據,說明上面源碼分析中的sqlType推斷是正確的了。

由此可以推斷出mybatis在空值處理這一塊是有相容性問題的。 

雖然mybatis在oracle資料庫時,遇到未賦值的空值會報錯,但是MySql資料庫卻不會報錯,

簡單的對mysql中對於空值處理做一個源碼分析吧

mybatis對於空值處理的部分都是一樣的,不一樣的是mysql驅動和oracle驅動對空值處理方式不一樣。

  這個預編譯對象指向mysql驅動的ClientPreparedStatement類。

後面就代碼是msyql對於空值的處理了;將會進入mysql驅動源碼的分析了。

setNull()源碼

  截圖中紅框註釋看到沒有:MySQL忽略sqlType。所以mybatis中給sqlType賦值成1111,對mysq來說解析空值完全沒有影響。

getCoreParameterIndex()源碼

1  protected final int getCoreParameterIndex(int paramIndex) throws SQLException {
2         int parameterIndexOffset = getParameterIndexOffset();
3         checkBounds(paramIndex, parameterIndexOffset);//這裡是對參數進行校驗,而且是值傳遞,並不會對這兩個值有任何修改的顧慮,就不進去看了
4         return paramIndex - 1 + parameterIndexOffset;//1-1+0
5     }

getParameterIndexOffset()源碼

1 //就是返回0,這是指定mysql參數解析的索引起始位置
2 protected int getParameterIndexOffset() {
3         return 0;
4     }

所以上面截圖的第1650行代碼中調用是下麵這樣子的

 ((PreparedQuery<?>) this.query).getQueryBindings().setNull(0); // MySQL ignores sqlType

這裡的0就是參數在mysql中的索引位置。

這裡從setNull()的調用方式來看,基本可以推斷出getQueryBindings()返回的是一個參數的對象,裡面包含了該參數的各種信息,提供給mysql資料庫進行解析參數使用;

這個對象也只有mysql資料庫能夠知道裡面各個欄位的意思(這個mysql驅動也是mysql資料庫提供的)

算了,還是繼續分析上面的setNull()方法吧。

 

 bindValues是一個數組,存放的是各個參數對象;

582行代碼就是調用第一個參數對象的setNull()方法;設置是否是空值。

至於setValue()我們繼續往下看。

setValue()源碼

1  public synchronized final void setValue(int paramIndex, String val, MysqlType type) {
2 //將參數值轉化成位元組數組
3         byte[] parameterAsBytes = StringUtils.getBytes(val, this.charEncoding);
4         setValue(paramIndex, parameterAsBytes, type);
5     }

這裡還有一個setValue()方法

public synchronized final void setValue(int paramIndex, byte[] val, MysqlType type) {
//參數對象設置位元組數組,實際上參數值就是以位元組數組的方式傳遞給資料庫的,並不是我們想象的1.2或者張三,李四
        this.bindValues[paramIndex].setByteValue(val);
//設置參數在mysql資料庫中數據類型,例如:varchar,int...
        this.bindValues[paramIndex].setMysqlType(type);
    }

到這位置從mybatis到mysql驅動的源碼分析總算是結束了。 

我很好奇在執行資料庫操作之前,mysql提供的預編譯器對象是個什麼樣子。

直接找到myBatis源碼的PreparedStatementHandler類

  這個ps就是我們要看的預編譯器對象。對象欄位實在太多只能分多個截圖了。

 

  bindValues就是我們剛纔源碼分析看到的值

 上圖顯示的內容是不是和我們分析的源碼完全一致。

從圖中可以看出這兩個參數是以兩個對象的方式存放在預編譯器中,傳遞給mysql資料庫,供mysql資料庫進行解析。

 

利用mybatis插入空值給資料庫;mysql能夠正常執行,而Oracle卻拋出異常;

這兩種截然不同的表現給程式員造成了困擾,那麼這個拋異常的鍋到底應該是誰來背呢?

當然是mybatis來背鍋嘍。oracle和mysql都根據jdbc介面來提供了自己的實現方法,

而mybatis作為一個封裝了JDBC的框架,沒有封裝到位,出現了相同的方法在不同資料庫的相容問題。

(ps:免費的框架天天用,大把大把的鈔票每月每月的領,還這樣埋怨mybatis,我覺得自己太不要臉嘍)

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 面向對象設計的SOLID原則 開放封閉原則(The Open Closed Principle) 一個軟體實體如類、模塊和函數應該對擴展開放,對修改代碼關閉。即軟體實體應儘量在不修改原有代碼的情況下進行擴展 [========] 軟體實體應該是可擴展,而不可修改的。也就是說,對擴展是開放的,而對修改 ...
  • 在基類定義演算法的結構,具體實現延遲到子類。 using System; namespace ConsoleApp2 { class Program { static void Main(string[] args) { TestPaper testPaperA = new TestPaperA(); ...
  • 代碼塊:被{ }包裹起來的代碼叫代碼塊 1.局部代碼塊:寫在方法中的代碼塊 2.構造代碼塊:寫在成員位置(類中方法外)的代碼塊 3.靜態代碼塊:寫在成員位置(類中方法外)被static修飾的代碼塊 一、局部代碼塊:寫在方法中的代碼塊 作用:修改變數的作用域,提高程式的效率 變數的作用域:在變數所在的 ...
  • 前言 我們經常會看到或使用InitializingBean(或@PostConstruct)進行Bean的一個初始化過程,但是有時候會發現InitializingBean存在一些不太適用的場景。 比如我們有以下一個Dog類 @Service @Scope(scopeName = Configurab ...
  • 在Java中提供了四種訪問許可權,使用不同的訪問許可權修飾符修飾時,被修飾的內容會有不同的訪問許可權。 public:公共的。 protected:受保護的 default:預設的(空的) private:私有的 可見,public具有最大許可權。private則是最小許可權。 編寫代碼時,如果沒有特殊的考慮 ...
  • A Lucky 7 題意:一個三位數,判但是否出現數字7. 題解:簽到題。 代碼: #include<iostream> #include<algorithm> #include<cstring> #include<stdio.h> using namespace std; int main(){ ...
  • ArrayList、LinkedList和Vector源碼分析 ArrayList ArrayList是一個底層使用數組來存儲對象,但不是線程安全的集合類 ArrayList的類結構關係 ArrayList實現了List介面,List介面中定義了一些對列表通過下標進行添加刪除等方法 ArrayLis ...
  • newFixedThreadPool @Slf4j public class TheadPoolDemo { private static ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newFixedThreadPoo ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...